From 5b374662d6b50806cfe79c1073b8986c3cc11614 Mon Sep 17 00:00:00 2001 From: shashankshampi Date: Wed, 30 Oct 2024 20:56:54 +0530 Subject: [PATCH] test_: Code Migration from status-cli-tests with review comments --- tests-functional/README.MD | 16 +---- tests-functional/clients/signals.py | 7 +-- tests-functional/constants.py | 6 +- tests-functional/requirements.txt | 58 +++++++++++++++++-- tests-functional/src/__init__.py | 0 tests-functional/src/libs/base_api_client.py | 1 + tests-functional/src/libs/common.py | 53 ++++++++++++++--- tests-functional/src/libs/custom_logger.py | 3 +- tests-functional/src/node/rpc_client.py | 4 -- tests-functional/src/node/status_node.py | 32 +++++----- tests-functional/src/steps/common.py | 22 ++----- .../tests/test_contact_request.py | 13 +---- 12 files changed, 134 insertions(+), 81 deletions(-) delete mode 100644 tests-functional/src/__init__.py diff --git a/tests-functional/README.MD b/tests-functional/README.MD index d30e44e00..d56e01450 100644 --- a/tests-functional/README.MD +++ b/tests-functional/README.MD @@ -1,7 +1,3 @@ -Here’s 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! \ No newline at end of file +In test build is automatically being build and placed in right path. If build already exists then new build is not generated. \ No newline at end of file diff --git a/tests-functional/clients/signals.py b/tests-functional/clients/signals.py index 192c50561..085ffbcd5 100644 --- a/tests-functional/clients/signals.py +++ b/tests-functional/clients/signals.py @@ -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__) diff --git a/tests-functional/constants.py b/tests-functional/constants.py index e6d3b732d..f8d67b227 100644 --- a/tests-functional/constants.py +++ b/tests-functional/constants.py @@ -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 \ No newline at end of file +NUM_CONTACT_REQUESTS = int(os.getenv("NUM_CONTACT_REQUESTS", "5")) \ No newline at end of file diff --git a/tests-functional/requirements.txt b/tests-functional/requirements.txt index 6d778112d..f168cc5bd 100644 --- a/tests-functional/requirements.txt +++ b/tests-functional/requirements.txt @@ -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 diff --git a/tests-functional/src/__init__.py b/tests-functional/src/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests-functional/src/libs/base_api_client.py b/tests-functional/src/libs/base_api_client.py index 0ae92d2a9..9eab3e288 100644 --- a/tests-functional/src/libs/base_api_client.py +++ b/tests-functional/src/libs/base_api_client.py @@ -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 diff --git a/tests-functional/src/libs/common.py b/tests-functional/src/libs/common.py index 4faa38473..2f2d8116f 100644 --- a/tests-functional/src/libs/common.py +++ b/tests-functional/src/libs/common.py @@ -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 diff --git a/tests-functional/src/libs/custom_logger.py b/tests-functional/src/libs/custom_logger.py index ec2f8e567..38bb98b31 100644 --- a/tests-functional/src/libs/custom_logger.py +++ b/tests-functional/src/libs/custom_logger.py @@ -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 diff --git a/tests-functional/src/node/rpc_client.py b/tests-functional/src/node/rpc_client.py index e8c4fd888..0b89f09d6 100644 --- a/tests-functional/src/node/rpc_client.py +++ b/tests-functional/src/node/rpc_client.py @@ -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", diff --git a/tests-functional/src/node/status_node.py b/tests-functional/src/node/status_node.py index dd0502764..85e5f6c87 100644 --- a/tests-functional/src/node/status_node.py +++ b/tests-functional/src/node/status_node.py @@ -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() diff --git a/tests-functional/src/steps/common.py b/tests-functional/src/steps/common.py index 6393cef27..5ce54e665 100644 --- a/tests-functional/src/steps/common.py +++ b/tests-functional/src/steps/common.py @@ -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 \ No newline at end of file + + return timestamp, message_id, response diff --git a/tests-functional/tests/test_contact_request.py b/tests-functional/tests/test_contact_request.py index 0490366e8..b1282eba8 100644 --- a/tests-functional/tests/test_contact_request.py +++ b/tests-functional/tests/test_contact_request.py @@ -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