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

test_: Code Migration from status-cli-tests #5990

Draft
wants to merge 12 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions tests-functional/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.idea/
.local/
96 changes: 74 additions & 22 deletions tests-functional/README.MD
Original file line number Diff line number Diff line change
@@ -1,41 +1,93 @@
## Overview

Functional tests for status-go
Functional tests for `status-go`

## Table of Contents

- [Overview](#overview)
- [How to Install](#how-to-install)
- [How to Run](#how-to-run)
- [Running Tests](#running-tests)
- [Implementation details](#implementation-details)
- [Implementation Details](#implementation-details)
- [Build Status Backend](#build-status-backend)

## How to Install

* Install [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/)
* Install [Python 3.10.14](https://www.python.org/downloads/)
* In `./tests-functional`, run `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/):
1. Install [Docker](https://docs.docker.com/engine/install/) and [Docker Compose](https://docs.docker.com/compose/install/)
2. Install [Python 3.10.14](https://www.python.org/downloads/)
3. **Set up a virtual environment (recommended):**
- In `./tests-functional`, run:
```bash
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/)

## How to Run

### Running dev RPC (anvil with contracts)
- In `./tests-functional` run `docker compose -f docker-compose.anvil.yml up --remove-orphans --build`, as result:
* an [anvil](https://book.getfoundry.sh/reference/anvil/) container with ChainID 31337 exposed on `0.0.0.0:8545` will start running
* Status-im contracts will be deployed to the network
### Running dev RPC (Anvil with contracts)

### Run tests
- In `./tests-functional` run `docker compose -f docker-compose.anvil.yml -f docker-compose.test.status-go.yml -f docker-compose.status-go.local.yml up --build --remove-orphans`, as result:
* a container with [status-go as daemon](https://github.com/status-im/status-go/issues/5175) will be created with APIModules exposed on `0.0.0.0:3333`
* status-go will use [anvil](https://book.getfoundry.sh/reference/anvil/) as RPCURL with ChainID 31337
* all Status-im contracts will be deployed to the network
In `./tests-functional`:
```bash
docker compose -f docker-compose.anvil.yml up --remove-orphans --build
```

* In `./tests-functional/tests` directory run `pytest -m wallet`
This command will:
- Start an [Anvil](https://book.getfoundry.sh/reference/anvil/) container with ChainID `31337`, exposed on `0.0.0.0:8545`
- Deploy Status-im contracts to the Anvil network

## Implementation details
### Running Tests

- Functional tests are implemented in `./tests-functional/tests` based on [pytest](https://docs.pytest.org/en/8.2.x/)
- Every test has two types of verifications:
- `verify_is_valid_json_rpc_response()` checks for status code 200, non-empty response, JSON-RPC structure, presence of the `result` field, and expected ID.
- `jsonschema.validate()` is used to check that the response contains expected data, including required fields and types. Schemas are stored in `/schemas/wallet_MethodName`
- New schemas can be generated using `./tests-functional/schema_builder.py` by passing a response to the `CustomSchemaBuilder(schema_name).create_schema(response.json())` method, should be used only on test creation phase, please search `how to create schema:` to see an example in a test
To run the tests:

1. In `./tests-functional`, start the testing containers:
```bash
docker compose -f docker-compose.anvil.yml -f docker-compose.test.status-go.yml -f docker-compose.status-go.local.yml up --build --remove-orphans
```

This command will:
- Create a container with [status-go as daemon](https://github.com/status-im/status-go/issues/5175), exposing `APIModules` on `0.0.0.0:3333`
- Configure `status-go` to use [Anvil](https://book.getfoundry.sh/reference/anvil/) as the `RPCURL` with ChainID `31337`
- Deploy all Status-im contracts to the Anvil network

2. To execute tests:
- Run all tests:
```bash
pytest
```
- Run tests marked as `wallet`:
```bash
pytest -m wallet
```
- Run a specific test:
```bash
pytest -k "test_contact_request_baseline"
```

## Implementation Details

- Functional tests are implemented in `./tests-functional/tests` using [pytest](https://docs.pytest.org/en/8.2.x/).
- Each test performs two types of verifications:
- **`verify_is_valid_json_rpc_response()`**: Checks for a status code `200`, a non-empty response, JSON-RPC structure, presence of the `result` field, and the expected ID.
- **`jsonschema.validate()`**: Validates that the response contains expected data, including required fields and types. Schemas are stored in `/schemas/wallet_MethodName`.

- **Schema Generation**:
- New schemas can be generated with `./tests-functional/schema_builder.py` by passing a response to the `CustomSchemaBuilder(schema_name).create_schema(response.json())` method. This should be used only during test creation.
- Search `how to create schema:` in test files for examples.

## Build Status Backend

You can manually build the binary with the following command in the `status-go` root directory:

```bash
make status-backend
```

For further details on building and setting up `status-go` and `status-backend`, refer to the official documentation:
- [status-backend README](https://github.com/status-im/status-go/blob/develop/cmd/status-backend/README.md)
- [status-go cmd directory](https://github.com/status-im/status-go/tree/develop/cmd/status-backend)

Location of the binary: `cmd/status-backend/status-backend`
shashankshampi marked this conversation as resolved.
Show resolved Hide resolved

In test build is automatically being build and placed in right path. If build already exists then new build is not generated.
57 changes: 36 additions & 21 deletions tests-functional/clients/signals.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,63 @@
import json
import logging
import time
from src.libs.common import write_signal_to_file

import websocket

logger = logging.getLogger(__name__)

class SignalClient:

def __init__(self, ws_url, await_signals):
self.url = f"{ws_url}/signals"

self.await_signals = await_signals
self.received_signals = {
signal: [] for signal in self.await_signals
}
self.received_signals = {signal: [] for signal in self.await_signals}

def on_message(self, ws, signal):
signal = json.loads(signal)
if signal.get("type") in self.await_signals:
self.received_signals[signal["type"]].append(signal)
signal_data = json.loads(signal)
signal_type = signal_data.get("type")

write_signal_to_file(signal_data)

def wait_for_signal(self, signal_type, timeout=20):
if signal_type in self.await_signals:
self.received_signals[signal_type].append(signal_data)

def wait_for_signal(self, signal_type, expected_event=None, timeout=20):
start_time = time.time()
while not self.received_signals.get(signal_type):
if time.time() - start_time >= timeout:
raise TimeoutError(
f"Signal {signal_type} is not received in {timeout} seconds")
while time.time() - start_time < timeout:
if self.received_signals.get(signal_type):
received_signal = self.received_signals[signal_type][0]
if expected_event:
event = received_signal.get("event", {})
if all(event.get(k) == v for k, v in expected_event.items()):
logger.info(f"Signal {signal_type} with event {expected_event} received and matched.")
return received_signal
else:
logger.debug(
f"Signal {signal_type} received but event did not match expected event: {expected_event}. Received event: {event}")
else:
logger.info(f"Signal {signal_type} received without specific event validation.")
return received_signal
time.sleep(0.2)
logging.debug(f"Signal {signal_type} is received in {round(time.time() - start_time)} seconds")
return self.received_signals[signal_type][0]

raise TimeoutError(f"Signal {signal_type} with event {expected_event} not received in {timeout} seconds")

def _on_error(self, ws, error):
logging.error(f"Error: {error}")
logger.error(f"WebSocket error: {error}")

def _on_close(self, ws, close_status_code, close_msg):
logging.info(f"Connection closed: {close_status_code}, {close_msg}")
logger.info(f"WebSocket connection closed: {close_status_code}, {close_msg}")

def _on_open(self, ws):
logging.info("Connection opened")
logger.info("WebSocket connection opened")

def _connect(self):
ws = websocket.WebSocketApp(self.url,
on_message=self.on_message,
on_error=self._on_error,
on_close=self._on_close)
ws = websocket.WebSocketApp(
self.url,
on_message=self.on_message,
on_error=self._on_error,
on_close=self._on_close
)
ws.on_open = self._on_open
ws.run_forever()
32 changes: 30 additions & 2 deletions tests-functional/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import os
import random
from dataclasses import dataclass

from src.libs.common import create_unique_data_dir

@dataclass
class Account:
Expand All @@ -8,7 +10,6 @@ class Account:
password: str
passphrase: str


user_1 = Account(
address="0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
private_key="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
Expand All @@ -21,3 +22,30 @@ class Account:
password="Strong12345",
passphrase="test test test test test test test test test test nest junk"
)

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")

DATA_DIR = os.path.join(PROJECT_ROOT, "tests-functional/local")
LOCAL_DATA_DIR1 = create_unique_data_dir(DATA_DIR, random.randint(1, 100))
LOCAL_DATA_DIR2 = create_unique_data_dir(DATA_DIR, random.randint(1, 100))
RESOURCES_FOLDER = os.path.join(PROJECT_ROOT, "resources")

ACCOUNT_PAYLOAD_DEFAULTS = {
"displayName": "user",
"password": "test_password",
"customizationColor": "primary"
}

LATENCY_CMD = "sudo tc qdisc add dev eth0 root netem delay 1s 100ms distribution normal"
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 = int(os.getenv("NUM_CONTACT_REQUESTS", "5"))
NUM_MESSAGES = int(os.getenv("NUM_MESSAGES", "25"))
DELAY_BETWEEN_MESSAGES = int(os.getenv("NUM_MESSAGES", "1"))
1 change: 1 addition & 0 deletions tests-functional/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pytest==6.2.4
requests==2.31.0
genson~=1.2.2
websocket-client~=1.4.2
tenacity==8.2.3
Empty file.
29 changes: 29 additions & 0 deletions tests-functional/src/libs/base_api_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import requests
import json
from tenacity import retry, stop_after_delay, wait_fixed
shashankshampi marked this conversation as resolved.
Show resolved Hide resolved
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

@retry(stop=stop_after_delay(10), wait=wait_fixed(0.5), reraise=True)
def send_post_request(self, endpoint, payload=None, headers=None, timeout=10):
if headers is None:
headers = {"Content-Type": "application/json"}
if payload is None:
payload = {}

url = f"{self.base_url}/{endpoint}"
logger.info(f"Sending POST request to {url} with payload: {json.dumps(payload)}")
try:
response = requests.post(url, headers=headers, data=json.dumps(payload), timeout=timeout)
response.raise_for_status()
logger.info(f"Response received: {response.status_code} - {response.text}")
return response.json()
except requests.exceptions.RequestException as e:
logger.error(f"Request to {url} failed: {str(e)}")
raise
65 changes: 65 additions & 0 deletions tests-functional/src/libs/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import json
from time import sleep
from src.libs.custom_logger import get_custom_logger
import subprocess
import shutil
import os
import uuid
from datetime import datetime
from pathlib import Path

logger = get_custom_logger(__name__)
GO_PROJECT_ROOT = Path(__file__).resolve().parents[3]
SOURCE_DIR = GO_PROJECT_ROOT / "cmd/status-backend"
fbarbu15 marked this conversation as resolved.
Show resolved Hide resolved
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:
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:
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
25 changes: 25 additions & 0 deletions tests-functional/src/libs/custom_logger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging

max_log_line_length = 10000


def log_length_filter(max_length):
class logLengthFilter(logging.Filter):
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}"
)
return False
return True

return logLengthFilter()


def get_custom_logger(name):
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("docker").setLevel(logging.WARNING)
logger = logging.getLogger(name)
logger.addFilter(log_length_filter(max_log_line_length))
return logger
Empty file.
Loading