Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Py/rc testing #2314

Merged
merged 13 commits into from
Sep 23, 2024
59 changes: 59 additions & 0 deletions .github/workflows/pypi-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,62 @@ jobs:
with:
command: upload
args: --skip-existing python/wheels/*

test-release:
if: github.event_name != 'pull_request'
name: Test the release
runs-on: ${{ matrix.build.RUNNER }}
needs: [publish-to-pypi, load-platform-matrix]
strategy:
fail-fast: false
matrix:
build: ${{ fromJson(needs.load-platform-matrix.outputs.PLATFORM_MATRIX) }}
steps:
- name: Setup self-hosted runner access
if: ${{ matrix.build.TARGET == 'aarch64-unknown-linux-gnu' }}
run: sudo chown -R $USER:$USER /home/ubuntu/actions-runner/_work/valkey-glide

- name: checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.12

- name: Install ValKey
uses: ./.github/workflows/install-valkey
with:
version: "8.0.0"

- name: Check if RC and set a distribution tag for the package
shell: bash
run: |
if [[ "${GITHUB_REF:11}" == *"rc"* ]]
then
echo "This is a release candidate"
export pip_pre="--pre"
else
echo "This is a stable release"
export pip_pre=""
fi
echo "PIP_PRE=${pip_pre}" >> $GITHUB_ENV

- name: Run the tests
shell: bash
working-directory: ./utils/release-candidate-testing/python
run: |
python -m venv venv
source venv/bin/activate
pip install -U pip
pip install ${PIP_PRE} valkey-glide
python rc_test.py

# Reset the repository to make sure we get the clean checkout of the action later in other actions.
# It is not required since in other actions we are cleaning before the action, but it is a good practice to do it here as well.
- name: Reset repository
if: ${{ contains(matrix.build.RUNNER, 'self-hosted') }}
shell: bash
run: |
git reset --hard
git clean -xdf
1 change: 1 addition & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
os: "ubuntu"
target: "x86_64-unknown-linux-gnu"
engine-version: ${{ matrix.engine.version }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- uses: Swatinem/rust-cache@v2

Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ extend-ignore = ['E203']
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']

[tool.mypy]
exclude = [ 'submodules' ]
exclude = [ 'submodules', 'utils/release-candidate-testing' ]
2 changes: 2 additions & 0 deletions utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
servers/
3 changes: 3 additions & 0 deletions utils/release-candidate-testing/node/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/
import { GlideClient, GlideClusterClient } from "@valkey/valkey-glide";
import { ValkeyCluster } from "../../TestUtils.js";

Expand Down
1 change: 1 addition & 0 deletions utils/release-candidate-testing/python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
208 changes: 208 additions & 0 deletions utils/release-candidate-testing/python/rc_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0

import asyncio
avifenesh marked this conversation as resolved.
Show resolved Hide resolved
from typing import List, Tuple
import os
import subprocess
import sys

SCRIPT_FILE = os.path.abspath(f"{__file__}/../../../cluster_manager.py")

from glide import (
GlideClusterClient,
GlideClusterClientConfiguration,
NodeAddress,
GlideClient,
GlideClientConfiguration,
)


def start_servers(cluster_mode: bool, shard_count: int, replica_count: int) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you re-use start/stop/parse from IT? No need to re-invent the wheel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python is very harsh in this regard, i need the envs to be aware of each other.
If i want to reuse i need to take those functions out to the root folder, create a hack, or call it through cli.
Since it's not really something we use, it's just a fofo test, I'm not sure it's worth it.
Especially if it will affect the efficiency of the IT development because it is extra dependent.
I'm open to hearing if you think differently or you know it's easy to do so.
But importing the base on the path doesn't work.

args_list: List[str] = [sys.executable, SCRIPT_FILE]
args_list.append("start")
if cluster_mode:
args_list.append("--cluster-mode")
args_list.append(f"-n {shard_count}")
args_list.append(f"-r {replica_count}")
p = subprocess.Popen(
args_list,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
output, err = p.communicate(timeout=40)
if p.returncode != 0:
raise Exception(f"Failed to create a cluster. Executed: {p}:\n{err}")
print("Servers started successfully")
return output


def parse_cluster_script_start_output(output: str) -> Tuple[List[NodeAddress], str]:
assert "CLUSTER_FOLDER" in output and "CLUSTER_NODES" in output
lines_output: List[str] = output.splitlines()
cluster_folder: str = ""
nodes_addr: List[NodeAddress] = []
for line in lines_output:
if "CLUSTER_FOLDER" in line:
splitted_line = line.split("CLUSTER_FOLDER=")
assert len(splitted_line) == 2
cluster_folder = splitted_line[1]
if "CLUSTER_NODES" in line:
nodes_list: List[NodeAddress] = []
splitted_line = line.split("CLUSTER_NODES=")
assert len(splitted_line) == 2
nodes_addresses = splitted_line[1].split(",")
assert len(nodes_addresses) > 0
for addr in nodes_addresses:
host, port = addr.split(":")
nodes_list.append(NodeAddress(host, int(port)))
nodes_addr = nodes_list
print("Cluster script output parsed successfully")
return nodes_addr, cluster_folder


def stop_servers(folder: str) -> str:
args_list: List[str] = [sys.executable, SCRIPT_FILE]
args_list.extend(["stop", "--cluster-folder", folder])
p = subprocess.Popen(
args_list,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
output, err = p.communicate(timeout=40)
if p.returncode != 0:
raise Exception(f"Failed to stop the cluster. Executed: {p}:\n{err}")
print("Servers stopped successfully")
return output


async def run_commands(client: GlideClient) -> None:
print("Executing commands")
# Set a bunch of keys
for i in range(100):
res = await client.set(f"foo{i}".encode(), b"bar")
if res != "OK":
print(res)
raise Exception(f"Unexpected set response, expected 'OK', got {res}")
print("Keys set successfully")
# Get the keys
for i in range(10):
val = await client.get(f"foo{i}")
if val != b"bar":
print(val)
raise Exception(f"Unexpected value, expected b'bar', got {val}")
print("Keys retrieved successfully")
# Run some various commands
pong = await client.ping()
if pong != b"PONG":
print(pong)
raise Exception(f"Unexpected ping response, expected b'PONG', got {pong}")
print(f"Ping successful: {pong}")
# Set a bunch of keys to delete
array_of_keys: List[bytes] = [f"foo{i}".encode() for i in range(1, 4)]
# delete the keys
deleted_keys_num = await client.delete(array_of_keys)
print(f"Deleted keys: {deleted_keys_num}")
# check that the correct number of keys were deleted
if deleted_keys_num != 3:
print(deleted_keys_num)
raise Exception(
f"Unexpected number of keys deleted, expected 3, got {deleted_keys_num}"
)
# check that the keys were deleted
for i in range(1, 4):
val = await client.get(f"foo{i}")
if val is not None:
print(val)
raise Exception(f"Unexpected value, expected None, got {val}")
print("Keys deleted successfully")

# Test INCR command
incr_key = b"counter"
await client.set(incr_key, b"0")
incr_result = await client.incr(incr_key)
if incr_result != 1:
raise Exception(f"Unexpected INCR result, expected 1, got {incr_result}")
print("INCR command successful")

# Test LPUSH and LRANGE commands
list_key = b"mylist"
await client.lpush(list_key, [b"world", b"hello"])
list_values = await client.lrange(list_key, 0, -1)
if list_values != [b"hello", b"world"]:
raise Exception(
f"Unexpected LRANGE result, expected [b'hello', b'world'], got {list_values}"
)
print("LPUSH and LRANGE commands successful")

# Test HSET and HGETALL commands
hash_key = b"myhash"
await client.hset(hash_key, {b"field1": b"value1", b"field2": b"value2"})
hash_values = await client.hgetall(hash_key)
if hash_values != {b"field1": b"value1", b"field2": b"value2"}:
raise Exception(
f"Unexpected HGETALL result, expected {{b'field1': b'value1', b'field2': b'value2'}}, got {hash_values}"
)
print("HSET and HGETALL commands successful")

print("All commands executed successfully")


async def create_cluster_client(
nodes_list: List[NodeAddress] = [("localhost", 6379)]
) -> GlideClusterClient:
addresses: List[NodeAddress] = nodes_list
config = GlideClusterClientConfiguration(
addresses=addresses,
client_name="test_cluster_client",
)
client = await GlideClusterClient.create(config)
print("Cluster client created successfully")
return client


async def create_standalone_client(server: List[NodeAddress]) -> GlideClient:
config = GlideClientConfiguration(
addresses=server,
client_name="test_standalone_client",
)
client = await GlideClient.create(config)
print("Standalone client created successfully")
return client


async def test_standalone_client() -> None:
print("Testing standalone client")
output = start_servers(False, 1, 1)
servers, folder = parse_cluster_script_start_output(output)
standalone_client = await create_standalone_client(servers)
await run_commands(standalone_client)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run all IT instead

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already had this discussion with Bar on the node rc tests.
That's not what those tests should do.
We have IT tests for a reason, and we have rc tests for another, the goal is just a slight check that nothing weird happened, mainly the compilation and packing.
Not to test all functionality.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't find missing exports with these tests. We found missing exports for node client just because I ran all IT on an RC (#2307).
For basic checks you can run benchmark - and again without re-inventing a wheel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to run IT against the packed packages we should do it regularly using the npm pack for example and use it as "artifact".
Testing it during release doesn't make sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that it might be needed, but not during release, not against all host types, and do it regularly locally using packing.

The "not inventing the wheel" in the case of using the benchmarks is really much more pain than gain.
You take three different components of the repo with three different use cases and try to jingle between them when python anyway doesn't like stuff that is not in its environment.

I think its an over engineering.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stop_servers(folder)
print("Standalone client test completed")


async def test_cluster_client() -> None:
print("Testing cluster client")
output = start_servers(True, 3, 1)
servers, folder = parse_cluster_script_start_output(output)
cluster_client = await create_cluster_client(servers)
await run_commands(cluster_client)
stop_servers(folder)
print("Cluster client test completed")


async def test_clients() -> None:
await test_cluster_client()
print("Cluster client test passed")
await test_standalone_client()
print("Standalone client test passed")


def main() -> None:
asyncio.run(test_clients())
print("All tests completed successfully")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions utils/release-candidate-testing/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
valkey-glide
avifenesh marked this conversation as resolved.
Show resolved Hide resolved
Loading