Skip to content

Commit

Permalink
Add Python testing framework to support Integration testing of the bl…
Browse files Browse the repository at this point in the history
…oom module + Add basic sanity tests

Signed-off-by: Karthik Subbarao <[email protected]>
  • Loading branch information
KarthikSubbarao committed Sep 10, 2024
1 parent 0d2ad8a commit 85e20fd
Show file tree
Hide file tree
Showing 7 changed files with 695 additions and 14 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
Cargo.lock
target
tests/.build
__pycache__
test-data
.attach_pid*
20 changes: 6 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ find . -name "libvalkey_bloom.so"
valkey-server --loadmodule ./target/release/libvalkey_bloom.so
```

Script to build, run tests, and for release
```
./build.sh
```

Client Usage
```
<redacted> % ./valkey-cli
Expand Down Expand Up @@ -106,17 +111,4 @@ db0:keys=1,expires=0,avg_ttl=0
1) "key"
127.0.0.1:6379> bf.exists key item
(integer) 1
```

```
16084:M 27 Apr 2024 02:23:15.759 * Legacy Redis Module ./target/debug/libvalkey_bloom.so found
16084:M 27 Apr 2024 02:23:15.760 * <bloom> Created new data type 'bloomtype'
16084:M 27 Apr 2024 02:23:15.760 * Module 'bloom' loaded from ./target/debug/libvalkey_bloom.so
16084:M 27 Apr 2024 02:23:15.760 * Server initialized
16084:M 27 Apr 2024 02:23:15.760 * Loading RDB produced by valkey version 255.255.255
16084:M 27 Apr 2024 02:23:15.760 * RDB age 5 seconds
16084:M 27 Apr 2024 02:23:15.760 * RDB memory usage when created 1.17 Mb
16084:M 27 Apr 2024 02:23:15.760 * <module> NOOP for now
16084:M 27 Apr 2024 02:23:15.763 * Done loading RDB, keys loaded: 1, keys expired: 0.
16084:M 27 Apr 2024 02:23:15.763 * DB loaded from disk: 0.003 seconds
```
```
70 changes: 70 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env sh

# Script to run format checks valkey-bloom module, build it and generate .so files, run unit and integration tests.

# Exit the script if any command fails
set -e

SCRIPT_DIR=$(pwd)
echo "Script Directory: $SCRIPT_DIR"

echo "Running cargo and clippy format checks..."
cargo fmt --check
cargo clippy --profile release --all-targets -- -D clippy::all

echo "Running cargo build release..."
cargo build --all --all-targets --release

# We have waiting on a new feature in the valkey-module-rs to be released which will allow unit testing of Valkey Rust Modules.
# echo "Running unit tests..."
# cargo test

# Ensure VERSION environment variable is set
if [ -z "$VERSION" ]; then

This comment has been minimized.

Copy link
@YueTang-Vanessa

YueTang-Vanessa Sep 13, 2024

Contributor

How did the VERSION get detected? Do we need to pass any parameter to this script? Or do I need to have a valkey-server running on the background?

echo "ERROR: VERSION environment variable is not set."
exit 1
fi

if [ "$VERSION" != "unstable" ] && [ "$VERSION" != "7.2.6" ] && [ "$VERSION" != "7.2.5" ] ; then
echo "ERROR: Unsupported version - $VERSIOn"
exit 1
fi

REPO_URL="https://github.com/valkey-io/valkey.git"
BINARY_PATH="tests/.build/binaries/$VERSION/valkey-server"

if [ -f "$BINARY_PATH" ] && [ -x "$BINARY_PATH" ]; then
echo "valkey-server binary '$BINARY_PATH' found."
else
echo "valkey-server binary '$BINARY_PATH' not found."
mkdir -p "tests/.build/binaries/$VERSION"
cd tests/.build
rm -rf valkey
git clone "$REPO_URL"
cd valkey
git checkout "$VERSION"
make
cp src/valkey-server ../binaries/$VERSION/
fi

REQUIREMENTS_FILE="requirements.txt"

# Check if pip is available
if command -v pip > /dev/null 2>&1; then
echo "Using pip to install packages..."
pip install -r "$SCRIPT_DIR/$REQUIREMENTS_FILE"
# Check if pip3 is available
elif command -v pip3 > /dev/null 2>&1; then
echo "Using pip3 to install packages..."
pip3 install -r "$SCRIPT_DIR/$REQUIREMENTS_FILE"
else
echo "Error: Neither pip nor pip3 is available. Please install Python package installer."
exit 1
fi

export MODULE_PATH="$SCRIPT_DIR/target/release/libvalkey_bloom.so"

echo "Running the integration tests..."
python3 -m pytest --cache-clear -v "$SCRIPT_DIR/tests/"

echo "Build and Integration Tests succeeded"
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
valkey
pytest==4
32 changes: 32 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import time
import pytest
from valkey import ResponseError
from valkey_test_case import ValkeyTestCase, ValkeyServerVersion
import logging
import os

class TestBloomBasic(ValkeyTestCase):

def get_custom_args(self):
self.set_server_version(ValkeyServerVersion.LATEST)
return {
'loadmodule': os.getenv('MODULE_PATH'),
}

def test_basic(self):
client = self.server.get_new_client()
module_list_data = client.execute_command('MODULE LIST')
module_list_count = len(module_list_data)
assert module_list_count == 1
module_loaded = False
for module in module_list_data:
if (module[b'name'] == b'bf'):
module_loaded = True
break
assert(module_loaded)
bf_add_result = client.execute_command('BF.ADD filter1 item1')
assert bf_add_result == 1
bf_exists_result = client.execute_command('BF.EXISTS filter1 item1')
assert bf_exists_result == 1
bf_exists_result = client.execute_command('BF.EXISTS filter1 item2')
assert bf_exists_result == 0
101 changes: 101 additions & 0 deletions tests/valkeytests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
This module is loaded for all tests, and attaches a global port tracker to every test. This solves
the case where there are parallel parameters.
"""

import pytest
import fcntl
import socket
import os
import tempfile
import random
import subprocess
import threading
from pathlib import Path

class PortTracker(object):
""" Provides "safe" to bind ports to valkey-server
Ports allocation is file base is protected. A port that was obtained via
`get_unused_port` will not be allocated to any other process. Ports are
de-allocated (protection removed) upon PortTracker exit, even if the socket
of the port in question was closed.
"""

CLUSTER_BUS_PORT_OFFSET = 10000
MIN_PORT = 5000
MAX_PORT = 32768 # this is the lower ephemeral port range
MAX_BASE_PORT = MAX_PORT - CLUSTER_BUS_PORT_OFFSET - MIN_PORT
MAX_RETRIES = 100
LOCKS_DIR = os.path.join(tempfile.gettempdir(), "port_tracker")


def __init__(self, node_id):
self._hash = hash(str(node_id))
if not os.path.exists(Path(PortTracker.LOCKS_DIR)):
Path(PortTracker.LOCKS_DIR).mkdir(parents=True, exist_ok=True)

def __enter__(self):
self.open_and_locked_files = {}
return self

def __exit__(self, type, value, tb):
for lockfile in self.open_and_locked_files.values():
self._try_remove(lockfile)

def _try_remove(self, lockfile):
lockfile.close()
try:
os.remove(lockfile.name)
except:
pass

def _next_port(self):
self._hash = hash(str(self._hash))
return (self._hash % PortTracker.MAX_BASE_PORT) + PortTracker.MIN_PORT

def _try_lock_port(self, port):
# get a lock on a file
lockfilename = os.path.join(self.LOCKS_DIR, "port%d.lock" % port)
lockfile = open(lockfilename, "w")
try:
fcntl.flock(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError:
self._try_remove(lockfile)
return False
# test that the valkey server would be able to bind to this port
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind(('0.0.0.0', port))
except OSError:
lockfile.close()
return False

self.open_and_locked_files[port] = lockfile
return True

def _unlock_port(self, port):
lockfile = self.open_and_locked_files.get(port)
if lockfile:
lockfile.close()
del self.open_and_locked_files[port]

def get_unused_port(self):
for r in range(PortTracker.MAX_RETRIES):
port = self._next_port()
if not self._try_lock_port(port):
continue
if not self._try_lock_port(port + PortTracker.CLUSTER_BUS_PORT_OFFSET):
self._unlock_port(port)
continue
return port
assert False , "Failed to find port after %d tries" % PortTracker.MAX_RETRIES

@pytest.fixture(scope='function', autouse=True)
def resource_port_tracker(request):
'''
Create port tracker for each pytest worker.
'''
with PortTracker(request.node.nodeid) as p:
yield p
Loading

0 comments on commit 85e20fd

Please sign in to comment.