Add ReuseServerTestCase for class-scoped server reuse - #13
Conversation
e94d1d4 to
5006368
Compare
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 <niakatf@amazon.com>
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 <niakatf@amazon.com>
de8c642 to
88d0f96
Compare
chinguyen21
left a comment
There was a problem hiding this comment.
Overall looks better a lot after the 2nd change. Just a few things to improve. Please check the comments
|
|
||
|
|
||
| class TestReuseServer(ReuseServerTestCase): | ||
| """Verifies that server reuse works and tests are isolated.""" |
There was a problem hiding this comment.
Lack of setup_test to call create_server() after redesigning the ReuseServerTestCase in the 2nd commit
There was a problem hiding this comment.
you're correct. After the redesign of the server reuse, the server is only created when create_server() is first called. I'll add a setup_test fixture taht calls create_server().
| @@ -1,4 +1,4 @@ | |||
| valkey | |||
| pytest==6 | |||
| pytest==7.4.3 | |||
There was a problem hiding this comment.
Why do we need this change? Doesn't pytest6 work?
There was a problem hiding this comment.
The pytest 6 works but it throws deprecation warnings for the teardown(self) method and pytest 7 handles fixture scoping more reliably (like our class-scoped class_teardown fixture.) but for simplicity I'll remove it.
| def teardown(self): | ||
| if hasattr(self.__class__, '_shared_server') and self.__class__._shared_server: | ||
| client = self.__class__._shared_client | ||
| client.flushall() |
There was a problem hiding this comment.
If the server somehow crashes during a test, this will raise an exception. Can we handle it? And make sure if exception happens, we should discard the shared server, so other tests can still run properly
There was a problem hiding this comment.
Fixed. I wrapped the teardown in a try/except. If the server crashes, we can just discard _shared_server and _shared_client by setting them to None. The next test's create_server will see None and spin up a new server for concurrent tests.
|
|
||
| def create_server( | ||
| self, | ||
| testdir=None, |
There was a problem hiding this comment.
Is there any benefit to not require testdir?
There was a problem hiding this comment.
Added a fallback: if testdir isn't passed, it defaults to self.testdir (same approach as server_path). This way None never reaches the parent, and modules that already pass testdir=self.testdir still work the same.
482ecaa to
53af29f
Compare
|
I let workflows run seems like there are some formatting issues can run |
|
|
||
|
|
||
| @pytest.fixture(scope="class") | ||
| def class_port_tracker(request): |
There was a problem hiding this comment.
Do we need this still? I saw we had an import for this but removed it
53af29f to
c3685d4
Compare
- 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 <niakatf@amazon.com>
1ecb724 to
eb44776
Compare
zackcam
left a comment
There was a problem hiding this comment.
Looking good! Still seems to have workflow failures with formatting hopefully should be easy to fix.
Do we also want to reset ACL? Script and function also might persist through? Potentially replication as well i.e made the server a replica should we check if we want to reset that?
| from valkey_test_case import ReuseServerTestCase | ||
|
|
||
|
|
||
| class TestReuseServer(ReuseServerTestCase): |
There was a problem hiding this comment.
For the tests I think they should run top to bottom? Should we make a note that this is how the ordering works somewhere?
There was a problem hiding this comment.
The tests already run top to bottom. I'll reference it explicitly in the README file and py file
|
|
||
|
|
||
| class TestReuseServer(ReuseServerTestCase): | ||
| """Verifies that server reuse works and tests are isolated.""" |
There was a problem hiding this comment.
Can we add a test for resetting a config?
| for key, val in self.__class__._initial_config.items(): | ||
| if current.get(key) != val: | ||
| try: | ||
| client.config_set(key, val) |
There was a problem hiding this comment.
As we catch here we silently could not reset a config. I think we should at least put a comment if this happens, or even this time actually tear down the server fully and restart one
There was a problem hiding this comment.
We should look at updating readme for this as well to show this new functionality
| client.config_set(key, val) | ||
| except Exception: | ||
| pass | ||
| except Exception: |
There was a problem hiding this comment.
Could this potentially leave behind process that won't be cleaned up?
- Add ReuseServerTestCase usage section to README - Add note about test ordering (top-to-bottom via pytest-order) - Log warning and tear down server when config_set fails to restore - Call exit() on server before discarding when teardown hits an exception - Add test_config_change_is_restored and test_config_restored_after_previous Signed-off-by: Fanta Niakate <niakatf@amazon.com>
Signed-off-by: Fanta Niakate <niakatf@amazon.com>
…down ReuseServerTestCase.teardown() now also resets: - REPLICAOF NO ONE (restore to primary if made replica) - SCRIPT FLUSH (clear cached Lua scripts) - FUNCTION FLUSH (clear loaded functions, no-op on Valkey < 7) - ACL DELUSER for non-default users + reset default user permissions Also adds explicit `import logging` instead of relying on wildcard import. Signed-off-by: Fanta Niakate <niakatf@amazon.com>
| assert self.client.execute_command("GET K") is None | ||
| ``` | ||
|
|
||
| `ReuseServerTestCase` inherits `ValkeyTestCase`, so all existing fixtures, `create_server()` calls, and `self.server`/`self.client` assignments work unchanged. To adopt it in your module, just change the base class — no other code changes needed. |
There was a problem hiding this comment.
I think the create_server() might do some funky things, do we tear it down properly as we override the teardown list? I think we also might return early here and not actually create a new server
There was a problem hiding this comment.
I think it should be fine. We don't have a teardown list as we will reuse the _shared_server unless it crashes somehow in previous test. We properly tear down the existing server in class_teardown. Though it's good point that our override teardown function is mainly doing the reset config.
@Fniakate8 We properly can add comment like Reset shared server state between tests instead of shutting it down. at the beginning of your teardown function, and properly creating a private method _reset_server_state and call it inside teardown.
There was a problem hiding this comment.
I was thinking more on the lines if we call create server in a test then we will need to tear down the extra one we created.
There was a problem hiding this comment.
The server cannot create a extra test, it just resets its configs, and if it can't reset it, it logs a warning, kills the server and creates a new one . But I just Extracted it into _reset_server_state() to make the intent more clear at a glance. ;)
There was a problem hiding this comment.
In that case the readme isn't quite right then, as the create_server would not return a new server if called in a test. If we are adding it to be the same we should allow the functionality to allow the user to call create server in a test and have it return a new server and teardown at the end of that test
There was a problem hiding this comment.
Discussed offline with @zackcam.
@Fniakate8 There are some tests in bloom and search modules which call create_server inside the test itself, then we might not properly clean them up. We should properly clean up all the servers in the list in class_teardown similar to the current teardown.
| def test_config_restored_after_previous(self): | ||
| """Proves the config changed in the previous test was reset.""" | ||
| current = self.client.config_get("hz")["hz"] | ||
| assert current == "10", f"Expected hz=10 (default), got hz={current}" |
There was a problem hiding this comment.
Not against hardcoding this but if its easy might be better to do where we capture the value of the config at startup but again small nit this shouldnt change i think
| username = entry.split(" ")[1] | ||
| client.execute_command("ACL", "DELUSER", username) | ||
| client.execute_command( | ||
| "ACL", "SETUSER", "default", "reset", "on", "~*", "&*", "+@all" |
There was a problem hiding this comment.
Default user should also not need a password from my understanding
|
|
||
| @pytest.fixture(autouse=True) | ||
| def setup_test(self, setup): | ||
| server_path = f"{os.path.dirname(os.path.realpath(__file__))}/.build/binaries/{os.environ['SERVER_VERSION']}/valkey-server" |
There was a problem hiding this comment.
For this if someone wants to run this without using build.sh we should default to checking unstable if they haven't set server version
…llback - Add nopass to ACL SETUSER default reset to ensure no password required - Extract _reset_server_state() from teardown for clarity - Add comment on create_server() explaining cached return behavior - Capture initial hz value in test instead of hardcoding - Fall back to "unstable" if SERVER_VERSION env var not set - Clarify README on caching and reset behavior Signed-off-by: Fanta Niakate <niakatf@amazon.com>
- Clean up additional servers in teardown() (skip shared server) - Add safety-net server_list cleanup in class_teardown with getattr - Add docstring to _reset_server_state explaining reset behavior - Update class docstring to accurately list all reset operations - Document additional server cleanup in README Signed-off-by: Fanta Niakate <niakatf@amazon.com>
db2c9b8 to
6e9aeaa
Compare
|
Sorry for churn found a couple more places we should reset which I missed before :( but code looks good to me now.
|
Addresses mentor feedback on additional state to reset between tests: - RESET the shared connection first (discards MULTI/WATCH, disables CLIENT TRACKING, restores RESP2/db0/READWRITE, etc.) so later commands aren't queued inside a lingering MULTI - CLIENT KILL TYPE normal to drop any connections a test spawned (SKIPME yes default keeps the shared client alive) - SLOWLOG RESET, LATENCY RESET, ACL LOG RESET so per-test log checks start clean Add tests proving a spawned connection and an ACL user left behind in one test are gone in the next, and sync the class/method/README/test docstrings with the full set of reset operations. Signed-off-by: Fanta Niakate <niakatf@amazon.com>
This PR implements server reuse for test classes, keeping one server alive across all tests in a class instead of spawning and killing a new process per test. Between tests the server is reset to a clean state so each test still starts as isolated.
What's Added
-
ReuseServerTestCaseinheritsValkeyTestCaseand overridescreate_server()to cache the server on the first call — subsequent calls return the cached instance_reset_server_state()restores full isolation:CLIENT KILL TYPEnormal to drop any connections a test spawned (SKIPME keeps the shared client alive)REPLICAOF NO ONE,FLUSHALL,CONFIG RESETSTAT,SCRIPT FLUSH,FUNCTION FLUSHHow to use
To adopt server reuse in your module:
Your existing setup_test, create_server() calls, and self.server/self.client assignments all work unchanged. Tests that need a fresh server (restart the server, change startup-only configs, do persistence operations)
should stay on ValkeyTestCase.
How I tested with other modules
pollute later tests thanks to the automatic config restore.
Server reuse helps most when module loading is expensive (like JSON) and tests are lightweight data operations. Bloom sees less improvement because its tests are compute-heavy (filling filters, sleeping for
expiration).
The framework's own tests/test_reuse_server.py also verifies the reset behavior directly: data isolation, config restore, and that a client connection + ACL user left behind in one test are gone in the next.
Test plan