test_: Code Migration from status-cli-tests with review comments

This commit is contained in:
shashankshampi 2024-10-30 20:56:54 +05:30
parent 5de833a617
commit 5b374662d6
12 changed files with 134 additions and 81 deletions

View File

@ -1,7 +1,3 @@
Heres the updated README with the additional prerequisites and instructions:
---
## Overview
Functional tests for `status-go`
@ -22,15 +18,11 @@ Functional tests for `status-go`
3. **Set up a virtual environment (recommended):**
- In `./tests-functional`, run:
```bash
python -m venv .venv
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
- **Optional (for test development)**: Use Python virtual environment for better dependency management. You can follow the guide [here](https://akrabat.com/creating-virtual-environments-with-pyenv/)
4. Install pre-commit hooks (optional):
```bash
pre-commit install
```
## How to Run
@ -86,7 +78,7 @@ To run the tests:
## Build Status Backend
You can build the binary with the following command in the `status-go` root directory:
You can manually build the binary with the following command in the `status-go` root directory:
```bash
make status-backend
@ -98,6 +90,4 @@ For further details on building and setting up `status-go` and `status-backend`,
Location of the binary: `cmd/status-backend/status-backend`
---
This README should cover your additional setup, installation, and testing instructions with clear steps for users. Let me know if there are any further modifications needed!
In test build is automatically being build and placed in right path. If build already exists then new build is not generated.

View File

@ -1,9 +1,11 @@
import json
import logging
import time
from src.libs.common import write_signal_to_file
import websocket
logger = logging.getLogger(__name__)
class SignalClient:
@ -13,16 +15,13 @@ class SignalClient:
self.received_signals = {signal: [] for signal in self.await_signals}
def on_message(self, ws, signal):
logger = logging.getLogger(__name__)
signal_data = json.loads(signal)
signal_type = signal_data.get("type")
logger.info(f"Received signal: {signal_data}")
write_signal_to_file(signal_data)
if signal_type in self.await_signals:
self.received_signals[signal_type].append(signal_data)
# logger.debug(f"Signal {signal_type} stored: {signal_data}")
def wait_for_signal(self, signal_type, expected_event=None, timeout=20):
logger = logging.getLogger(__name__)

View File

@ -29,6 +29,10 @@ PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
STATUS_BACKEND_URL = os.getenv("STATUS_BACKEND_URL", "http://127.0.0.1")
API_REQUEST_TIMEOUT = int(os.getenv("API_REQUEST_TIMEOUT", "15"))
SOURCE_DIR = os.path.join(PROJECT_ROOT, "cmd/status-backend")
DEST_DIR = os.path.join(PROJECT_ROOT, "tests-functional")
BINARY_PATH = os.path.join(SOURCE_DIR, "status-backend")
# Paths relative to project root
DATA_DIR = os.path.join(PROJECT_ROOT, "tests-functional/local")
LOCAL_DATA_DIR1 = create_unique_data_dir(DATA_DIR, random.randint(1, 100))
@ -47,4 +51,4 @@ LATENCY_CMD = "sudo tc qdisc add dev eth0 root netem delay 1s 100ms distribution
PACKET_LOSS_CMD = "sudo tc qdisc add dev eth0 root netem loss 50%"
LOW_BANDWIDTH_CMD = "sudo tc qdisc add dev eth0 root tbf rate 1kbit burst 1kbit"
REMOVE_TC_CMD = "sudo tc qdisc del dev eth0 root"
NUM_CONTACT_REQUESTS = 5
NUM_CONTACT_REQUESTS = int(os.getenv("NUM_CONTACT_REQUESTS", "5"))

View File

@ -1,6 +1,56 @@
anyio==4.6.0
attrs==23.1.0
black==24.3.0
certifi==2023.11.17
cfgv==3.4.0
charset-normalizer==3.3.2
click==8.1.7
clients==1.5
deepdiff==5.5.0
jsonschema~=3.2.0
pytest==6.2.4
distlib==0.3.8
exceptiongroup==1.2.2
execnet==2.0.2
filelock==3.13.1
genson==1.2.2
h11==0.14.0
httpcore==1.0.6
httpx==0.27.2
identify==2.5.33
idna==3.7
importlib_metadata==8.5.0
iniconfig==2.0.0
jsonschema==3.2.0
libs==0.0.10
mypy-extensions==1.0.0
nodeenv==1.8.0
ordered-set==4.0.2
packaging==23.2
pathspec==0.12.1
platformdirs==4.1.0
pluggy==1.5.0
pre-commit==3.6.2
py==1.11.0
pyright==1.1.352
pyrsistent==0.20.0
pytest==8.3.3
pytest-dependency==0.6.0
pytest-instafail==0.5.0
pytest-rerunfailures==13.0
pytest-timeout==2.2.0
pytest-xdist==3.5.0
python-dotenv==1.0.1
PyYAML==6.0.1
requests==2.31.0
genson~=1.2.2
websocket-client~=1.4.2
signals==0.0.2
six==1.16.0
sniffio==1.3.1
tenacity==8.2.3
toml==0.10.2
tomli==2.0.2
typeguard==4.1.5
typing-inspect==0.9.0
typing_extensions==4.9.0
urllib3==2.1.0
virtualenv==20.25.0
websocket-client==1.4.2
zipp==3.20.2

View File

@ -5,6 +5,7 @@ from src.libs.custom_logger import get_custom_logger
logger = get_custom_logger(__name__)
class BaseAPIClient:
def __init__(self, base_url):
self.base_url = base_url

View File

@ -1,28 +1,65 @@
import json
from time import sleep
from src.libs.custom_logger import get_custom_logger
import subprocess
import shutil
import os
import allure
import uuid
from datetime import datetime
from pathlib import Path
logger = get_custom_logger(__name__)
def attach_allure_file(file):
logger.debug(f"Attaching file {file}")
allure.attach.file(file, name=os.path.basename(file), attachment_type=allure.attachment_type.TEXT)
GO_PROJECT_ROOT = Path(__file__).resolve().parents[3]
SOURCE_DIR = GO_PROJECT_ROOT / "cmd/status-backend"
DEST_DIR = GO_PROJECT_ROOT / "tests-functional"
BINARY_PATH = SOURCE_DIR / "status-backend"
REPORTS_DIR = DEST_DIR / "reports"
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
LOG_FILE_PATH = REPORTS_DIR / f"signals_log_{timestamp}.json"
def delay(num_seconds):
logger.debug(f"Sleeping for {num_seconds} seconds")
sleep(num_seconds)
def create_unique_data_dir(base_dir: str, index: int) -> str:
"""Generate a unique data directory for each node instance."""
unique_id = str(uuid.uuid4())[:8]
unique_dir = os.path.join(base_dir, f"data_{index}_{unique_id}")
os.makedirs(unique_dir, exist_ok=True)
return unique_dir
def get_project_root() -> str:
"""Returns the root directory of the project."""
return os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
def write_signal_to_file(signal_data):
with open(LOG_FILE_PATH, "a") as file:
json.dump(signal_data, file)
file.write("\n")
def build_and_copy_binary():
logger.info(f"Building status-backend binary in {GO_PROJECT_ROOT}")
result = subprocess.run(["make", "status-backend"], cwd=GO_PROJECT_ROOT, capture_output=True, text=True)
if result.returncode != 0:
logger.info("Build failed with the following output:")
logger.info(result.stderr)
return False
if not os.path.exists(BINARY_PATH):
logger.info("Binary build failed or not found! Exiting.")
return False
logger.info(f"Copying binary to {DEST_DIR}")
shutil.copy(BINARY_PATH, DEST_DIR)
if os.path.exists(os.path.join(DEST_DIR, "status-backend")):
logger.info("Binary successfully copied to tests-functional directory.")
return True
else:
logger.info("Failed to copy binary to the tests-functional directory.")
return False

View File

@ -8,7 +8,8 @@ def log_length_filter(max_length):
def filter(self, record):
if len(record.getMessage()) > max_length:
logging.getLogger(record.name).log(
record.levelno, f"Log line was discarded because it's longer than max_log_line_length={max_log_line_length}"
record.levelno,
f"Log line was discarded because it's longer than max_log_line_length={max_log_line_length}"
)
return False
return True

View File

@ -17,7 +17,6 @@ class StatusNodeRPC(BaseAPIClient):
reraise=True
)
def send_rpc_request(self, method, params=None, timeout=API_REQUEST_TIMEOUT):
"""Send JSON-RPC requests, used for standard JSON-RPC API calls."""
payload = {"jsonrpc": "2.0", "method": method, "params": params or [], "id": 1}
logger.info(f"Sending JSON-RPC request to {self.base_url} with payload: {payload}")
@ -35,7 +34,6 @@ class StatusNodeRPC(BaseAPIClient):
reraise=True
)
def initialize_application(self, data_dir, timeout=API_REQUEST_TIMEOUT):
"""Send a direct POST request to the InitializeApplication endpoint."""
payload = {"dataDir": data_dir}
logger.info(f"Sending direct POST request to InitializeApplication with payload: {payload}")
@ -53,7 +51,6 @@ class StatusNodeRPC(BaseAPIClient):
reraise=True
)
def create_account_and_login(self, account_data, timeout=API_REQUEST_TIMEOUT):
"""Send a direct POST request to CreateAccountAndLogin endpoint."""
payload = {
"rootDataDir": account_data.get("rootDataDir"),
"displayName": account_data.get("displayName", "test1"),
@ -76,7 +73,6 @@ class StatusNodeRPC(BaseAPIClient):
reraise=True
)
def start_messenger(self, timeout=API_REQUEST_TIMEOUT):
"""Send JSON-RPC request to start Waku messenger."""
payload = {
"jsonrpc": "2.0",
"method": "wakuext_startMessenger",

View File

@ -1,5 +1,4 @@
import os
import asyncio
import random
import shutil
import signal
@ -13,11 +12,17 @@ from conftest import option
from src.libs.custom_logger import get_custom_logger
from src.node.rpc_client import StatusNodeRPC
from clients.signals import SignalClient
from src.libs.common import build_and_copy_binary
from pathlib import Path
logger = get_custom_logger(__name__)
PROJECT_ROOT = Path(__file__).resolve().parents[2]
class StatusNode:
binary_built = False
def __init__(self, name=None, port=None, pubkey=None):
self.data_dir = None
try:
@ -37,9 +42,7 @@ class StatusNode:
self.api = StatusNodeRPC(self.port, self.name)
def setup_method(self):
# Set up RPC client
self.rpc_client = RpcClient(option.rpc_url_statusd)
# Set up WebSocket signal client
await_signals = ["history.request.started", "history.request.completed"]
self.signal_client = SignalClient(option.ws_url_statusd, await_signals)
@ -49,7 +52,6 @@ class StatusNode:
websocket_thread.start()
def initialize_node(self, name, port, data_dir, account_data):
"""Centralized method to initialize a node."""
self.name = name
self.port = port
self.start(data_dir)
@ -59,14 +61,19 @@ class StatusNode:
self.pubkey = self.get_pubkey(account_data["displayName"])
def start_node(self, command):
"""Start the node using a subprocess command."""
logger.info(f"Starting node with command: {command}")
self.process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
self.pid = self.process.pid
self.log_thread = self.capture_process_logs(self.process, self.logs)
def start(self, data_dir, capture_logs=True):
"""Start the status-backend node and initialize it before subscribing to signals."""
dest_binary_path = Path(PROJECT_ROOT) / "status-backend"
if not StatusNode.binary_built and not dest_binary_path.exists():
if not build_and_copy_binary():
raise RuntimeError("Failed to build or copy the status-backend binary.")
StatusNode.binary_built = True
self.capture_logs = capture_logs
self.data_dir = data_dir
command = ["./status-backend", f"--address=localhost:{self.port}"]
@ -77,17 +84,14 @@ class StatusNode:
self.start_signal_client()
def create_account_and_login(self, account_data):
"""Create an account and log in using the status-backend."""
logger.info(f"Creating account and logging in for node {self.name}")
self.api.create_account_and_login(account_data)
def start_messenger(self):
"""Start the Waku messenger."""
logger.info(f"Starting Waku messenger for node {self.name}")
self.api.start_messenger()
def start_signal_client(self):
"""Start a SignalClient for the given node to listen for WebSocket signals."""
ws_url = f"ws://localhost:{self.port}"
await_signals = ["history.request.started", "history.request.completed"]
self.signal_client = SignalClient(ws_url, await_signals)
@ -98,10 +102,9 @@ class StatusNode:
logger.info("WebSocket client started and subscribed to signals.")
def wait_fully_started(self):
"""Wait until the node logs indicate that the server has started."""
logger.info(f"Waiting for {self.name} to fully start...")
start_time = time.time()
while time.time() - start_time < 20:
while time.time() - start_time < 30:
if any("server started" in log for log in self.logs):
logger.info(f"Node {self.name} has fully started.")
return
@ -109,8 +112,6 @@ class StatusNode:
raise TimeoutError(f"Node {self.name} did not fully start in time.")
def capture_process_logs(self, process, logs):
"""Capture logs from a subprocess."""
def read_output():
while True:
line = process.stdout.readline()
@ -125,14 +126,11 @@ class StatusNode:
return log_thread
def random_node_name(self, length=10):
"""Generate a random node name."""
allowed_chars = string.ascii_lowercase + string.digits + "_-"
return ''.join(random.choice(allowed_chars) for _ in range(length))
def get_pubkey(self, display_name):
"""Retrieve public-key based on display name from accounts_getAccounts response."""
response = self.api.send_rpc_request("accounts_getAccounts")
accounts = response.get("result", [])
for account in accounts:
if account.get("name") == display_name:
@ -140,11 +138,9 @@ class StatusNode:
raise ValueError(f"Public key not found for display name: {display_name}")
def wait_for_signal(self, signal_type, expected_event=None, timeout=20):
"""Wait for a signal using the signal client and validate against expected event details."""
return self.signal_client.wait_for_signal(signal_type, expected_event, timeout)
def stop(self, remove_local_data=True):
"""Stop the status-backend process."""
if self.process:
logger.info(f"Stopping node with name: {self.name}")
self.process.kill()

View File

@ -12,19 +12,6 @@ logger = get_custom_logger(__name__)
class StepsCommon:
@pytest.fixture(scope="function", autouse=False)
def start_1_node(self):
account_data = {
**ACCOUNT_PAYLOAD_DEFAULTS,
"rootDataDir": LOCAL_DATA_DIR1,
"displayName": "first_node_user"
}
random_port = str(random.randint(1024, 65535))
self.first_node = StatusNode()
self.first_node.initialize_node("first_node", random_port, LOCAL_DATA_DIR1, account_data)
self.first_node_pubkey = self.first_node.get_pubkey()
@pytest.fixture(scope="function", autouse=False)
def start_2_nodes(self):
logger.debug(f"Running fixture setup: {inspect.currentframe().f_code.co_name}")
@ -64,7 +51,6 @@ class StepsCommon:
@contextmanager
def add_latency(self):
"""Add network latency"""
logger.debug("Adding network latency")
subprocess.Popen(LATENCY_CMD, shell=True)
try:
@ -75,7 +61,6 @@ class StepsCommon:
@contextmanager
def add_packet_loss(self):
"""Add packet loss"""
logger.debug("Adding packet loss")
subprocess.Popen(PACKET_LOSS_CMD, shell=True)
try:
@ -86,7 +71,6 @@ class StepsCommon:
@contextmanager
def add_low_bandwidth(self):
"""Add low bandwidth"""
logger.debug("Adding low bandwidth")
subprocess.Popen(LOW_BANDWIDTH_CMD, shell=True)
try:
@ -108,10 +92,12 @@ class StepsCommon:
def send_with_timestamp(self, send_method, id, message):
timestamp = datetime.now().strftime("%H:%M:%S")
response = send_method(id, message)
response_messages = response["result"]["messages"]
response_messages = response.get("result", {}).get("messages", [])
message_id = None
for m in response_messages:
if m["text"] == message:
message_id = m["id"]
break
return timestamp, message_id
return timestamp, message_id, response

View File

@ -53,7 +53,6 @@ class TestContactRequest(StepsCommon):
nodes.append((first_node, second_node, account_data_first["displayName"], index))
# Validate contact requests
missing_contact_requests = []
for first_node, second_node, display_name, index in nodes:
result = self.send_and_wait_for_message((first_node, second_node), display_name, index, timeout_secs)
@ -83,18 +82,12 @@ class TestContactRequest(StepsCommon):
first_node_pubkey = first_node.get_pubkey(display_name)
contact_request_message = f"contact_request_{index}"
timestamp, message_id = self.send_with_timestamp(
timestamp, message_id, response = self.send_with_timestamp(
second_node.send_contact_request, first_node_pubkey, contact_request_message
)
response = second_node.send_contact_request(first_node_pubkey, contact_request_message)
expected_event_started = {"requestId": "", "peerId": "", "batchIndex": 0, "numBatches": 1}
expected_event_completed = {"requestId": "", "peerId": "", "batchIndex": 0}
try:
first_node.wait_for_signal("history.request.started", expected_event_started, timeout)
first_node.wait_for_signal("history.request.completed", expected_event_completed, timeout)
first_node.wait_for_signal("history.request.started", None, timeout)
first_node.wait_for_signal("history.request.completed", None, timeout)
except TimeoutError as e:
logging.error(f"Signal validation failed: {str(e)}")
return timestamp, message_id, contact_request_message, None