Skip to content

Commit

Permalink
test_: add python linters
Browse files Browse the repository at this point in the history
  • Loading branch information
fbarbu15 committed Dec 13, 2024
1 parent 74db631 commit 068248e
Show file tree
Hide file tree
Showing 29 changed files with 497 additions and 291 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/pytest-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Pytest Lint

on:
pull_request:
branches:
- master
- test-linting

jobs:
pytest-lint:
timeout-minutes: 10
runs-on: ubuntu-latest
steps:

- uses: actions/checkout@v4

- uses: actions/setup-python@v4
with:
python-version: '3.12'
cache: 'pip'

- name: Set up virtual environment in /tests-functional/
run: |
python -m venv tests-functional/.venv
echo "tests-functional/.venv/bin" >> $GITHUB_PATH # Add virtualenv to PATH for subsequent steps
- name: Install dependencies based on requirements.txt
run: pip install -r requirements.txt

- name: Run pytest-lint from Makefile
run: make pytest-lint
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ tests-functional/coverage
tests-functional/reports
tests-functional/signals
tests-functional/*.log
pyrightconfig.json
.venv


Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -428,3 +428,8 @@ run-anvil:
codecov-validate: SHELL := /bin/sh
codecov-validate:
curl -X POST --data-binary @.codecov.yml https://codecov.io/validate

.PHONY: pytest-lint
pytest-lint:
@echo "Running python linting on all files..."
pre-commit run --all-files --verbose --config tests-functional/.pre-commit-config.yaml
6 changes: 6 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"include": ["tests-functional"],
"venvPath": "./tests-functional", // Ensure the virtual environment (.venv) is located in ./tests-functional
"venv": ".venv",
"typeCheckingMode": "off"
}
31 changes: 31 additions & 0 deletions tests-functional/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
repos:
- repo: https://github.com/psf/black
rev: 24.10.0 # Latest version of Black
hooks:
- id: black
args: [--line-length=150]
files: ^tests-functional/.*\.py$
# Black: Automatically formats Python code to adhere to its strict style guidelines.
# - Ensures consistent code formatting across the project.
# - Helps maintain readability and avoids debates about style in code reviews.

- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.388 # Version of Pyright used
hooks:
- id: pyright
files: ^tests-functional/.*\.py$
# Pyright: A static type checker for Python.
# - Validates type hints and ensures type correctness in code.
# - Identifies type mismatches, missing imports, and potential runtime errors.
# - Ensures better type safety and helps catch bugs early.

- repo: https://github.com/pycqa/flake8
rev: 7.1.1 # Latest version of Flake8
hooks:
- id: flake8
args: ["--max-line-length=150"]
files: ^tests-functional/.*\.py$
# Flake8: A lightweight Python linter for style and syntax checking.
# - Detects unused imports, undefined variables, and redefined functions (e.g., F811).
# - Checks for adherence to Python coding standards (PEP 8).
# - Helps maintain clean, bug-free code.
16 changes: 12 additions & 4 deletions tests-functional/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ Functional tests for status-go

## 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](https://www.python.org/downloads/) (tested with 3.10 and 3.12 and it works with both)
3. **Set up a virtual environment (required for linting):**
- In `./tests-functional`, run:
```bash
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
pre-commit install
```
- **Important**: The virtual environment must be created in the `./tests-functional` directory for pre-commit linting and type-checking tools like Pyright to work correctly.
- **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

Expand Down
13 changes: 4 additions & 9 deletions tests-functional/clients/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@ def _check_decode_and_key_errors_in_response(self, response, key):
try:
return response.json()[key]
except json.JSONDecodeError:
raise AssertionError(
f"Invalid JSON in response: {response.content}")
raise AssertionError(f"Invalid JSON in response: {response.content}")
except KeyError:
raise AssertionError(
f"Key '{key}' not found in the JSON response: {response.content}")
raise AssertionError(f"Key '{key}' not found in the JSON response: {response.content}")

def verify_is_valid_json_rpc_response(self, response, _id=None):
assert response.status_code == 200, f"Got response {response.content}, status code {response.status_code}"
Expand All @@ -31,9 +29,7 @@ def verify_is_valid_json_rpc_response(self, response, _id=None):
if _id:
try:
if _id != response.json()["id"]:
raise AssertionError(
f"got id: {response.json()['id']} instead of expected id: {_id}"
)
raise AssertionError(f"got id: {response.json()['id']} instead of expected id: {_id}")
except KeyError:
raise AssertionError(f"no id in response {response.json()}")
return response
Expand Down Expand Up @@ -69,5 +65,4 @@ def rpc_valid_request(self, method, params=None, _id=None, url=None):

def verify_json_schema(self, response, method):
with open(f"{option.base_dir}/schemas/{method}", "r") as schema:
jsonschema.validate(instance=response,
schema=json.load(schema))
jsonschema.validate(instance=response, schema=json.load(schema))
2 changes: 1 addition & 1 deletion tests-functional/clients/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

class Service:
def __init__(self, client: RpcClient, name: str):
assert name is not ""
assert name != ""
self.rpc_client = client
self.name = name

Expand Down
33 changes: 18 additions & 15 deletions tests-functional/clients/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from datetime import datetime
from enum import Enum


class SignalType(Enum):
MESSAGES_NEW = "messages.new"
MESSAGE_DELIVERED = "message.delivered"
Expand All @@ -22,6 +23,7 @@ class SignalType(Enum):
WALLET_TRANSACTION_STATUS_CHANGED = "wallet.transaction.status-changed"
WALLET_ROUTER_TRANSACTIONS_SENT = "wallet.router.transactions-sent"


class SignalClient:
def __init__(self, ws_url, await_signals):
self.url = f"{ws_url}/signals"
Expand All @@ -37,11 +39,15 @@ def __init__(self, ws_url, await_signals):
"received": [],
"delta_count": 1,
"expected_count": 1,
"accept_fn": None
} for signal in self.await_signals
"accept_fn": None,
}
for signal in self.await_signals
}
if LOG_SIGNALS_TO_FILE:
self.signal_file_path = os.path.join(SIGNALS_DIR, f"signal_{ws_url.split(':')[-1]}_{datetime.now().strftime('%H%M%S')}.log")
self.signal_file_path = os.path.join(
SIGNALS_DIR,
f"signal_{ws_url.split(':')[-1]}_{datetime.now().strftime('%H%M%S')}.log",
)
Path(SIGNALS_DIR).mkdir(parents=True, exist_ok=True)

def on_message(self, ws, signal):
Expand Down Expand Up @@ -77,8 +83,7 @@ def wait_for_signal(self, signal_type, timeout=20):
received_signals = self.received_signals.get(signal_type)
while (not received_signals) or len(received_signals["received"]) < received_signals["expected_count"]:
if time.time() - start_time >= timeout:
raise TimeoutError(
f"Signal {signal_type} is not received in {timeout} seconds")
raise TimeoutError(f"Signal {signal_type} is not received in {timeout} seconds")
time.sleep(0.2)
logging.debug(f"Signal {signal_type} is received in {round(time.time() - start_time)} seconds")
delta_count = received_signals["delta_count"]
Expand All @@ -97,17 +102,13 @@ def find_signal_containing_pattern(self, signal_type, event_pattern, timeout=20)
start_time = time.time()
while True:
if time.time() - start_time >= timeout:
raise TimeoutError(
f"Signal {signal_type} containing {event_pattern} is not received in {timeout} seconds"
)
raise TimeoutError(f"Signal {signal_type} containing {event_pattern} is not received in {timeout} seconds")
if not self.received_signals.get(signal_type):
time.sleep(0.2)
continue
for event in self.received_signals[signal_type]["received"]:
if event_pattern in str(event):
logging.info(
f"Signal {signal_type} containing {event_pattern} is received in {round(time.time() - start_time)} seconds"
)
logging.info(f"Signal {signal_type} containing {event_pattern} is received in {round(time.time() - start_time)} seconds")
return event
time.sleep(0.2)

Expand All @@ -121,10 +122,12 @@ def _on_open(self, ws):
logging.info("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()

Expand Down
47 changes: 26 additions & 21 deletions tests-functional/clients/status_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ def __init__(self, await_signals=[]):
url = f"http://127.0.0.1:{host_port}"
option.status_backend_port_range.remove(host_port)


self.api_url = f"{url}/statusgo"
self.ws_url = f"{url}".replace("http", "ws")
self.rpc_url = f"{url}/statusgo/CallRPC"
Expand Down Expand Up @@ -59,7 +58,8 @@ def _start_container(self, host_port):
"labels": {"com.docker.compose.project": docker_project_name},
"entrypoint": [
"status-backend",
"--address", "0.0.0.0:3333",
"--address",
"0.0.0.0:3333",
],
"ports": {"3333/tcp": host_port},
"environment": {
Expand All @@ -78,8 +78,7 @@ def _start_container(self, host_port):

container = self.docker_client.containers.run(**container_args)

network = self.docker_client.networks.get(
f"{docker_project_name}_default")
network = self.docker_client.networks.get(f"{docker_project_name}_default")
network.connect(container)

option.status_backend_containers.append(container.id)
Expand All @@ -99,8 +98,7 @@ def _health_check(self):
def api_request(self, method, data, url=None):
url = url if url else self.api_url
url = f"{url}/{method}"
logging.info(
f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
response = requests.post(url, json=data)
logging.info(f"Got response: {response.content}")
return response
Expand All @@ -113,8 +111,7 @@ def verify_is_valid_api_response(self, response):
error = response.json()["error"]
assert not error, f"Error: {error}"
except json.JSONDecodeError:
raise AssertionError(
f"Invalid JSON in response: {response.content}")
raise AssertionError(f"Invalid JSON in response: {response.content}")
except KeyError:
pass

Expand All @@ -133,7 +130,12 @@ def init_status_backend(self, data_dir=USER_DIR):
}
return self.api_valid_request(method, data)

def create_account_and_login(self, data_dir=USER_DIR, display_name=DEFAULT_DISPLAY_NAME, password=user_1.password):
def create_account_and_login(
self,
data_dir=USER_DIR,
display_name=DEFAULT_DISPLAY_NAME,
password=user_1.password,
):
method = "CreateAccountAndLogin"
data = {
"rootDataDir": data_dir,
Expand All @@ -146,8 +148,13 @@ def create_account_and_login(self, data_dir=USER_DIR, display_name=DEFAULT_DISPL
}
return self.api_valid_request(method, data)

def restore_account_and_login(self, data_dir=USER_DIR, display_name=DEFAULT_DISPLAY_NAME, user=user_1,
network_id=31337):
def restore_account_and_login(
self,
data_dir=USER_DIR,
display_name=DEFAULT_DISPLAY_NAME,
user=user_1,
network_id=31337,
):
method = "RestoreAccountAndLogin"
data = {
"rootDataDir": data_dir,
Expand All @@ -172,9 +179,9 @@ def restore_account_and_login(self, data_dir=USER_DIR, display_name=DEFAULT_DISP
"NativeCurrencyDecimals": 18,
"IsTest": False,
"Layer": 1,
"Enabled": True
"Enabled": True,
}
]
],
}
return self.api_valid_request(method, data)

Expand All @@ -197,22 +204,21 @@ def restore_account_and_wait_for_rpc_client_to_start(self, timeout=60):
# ToDo: change this part for waiting for `node.login` signal when websockets are migrated to StatusBackend
while time.time() - start_time <= timeout:
try:
self.rpc_valid_request(method='accounts_getKeypairs')
self.rpc_valid_request(method="accounts_getKeypairs")
return
except AssertionError:
time.sleep(3)
raise TimeoutError(
f"RPC client was not started after {timeout} seconds")
raise TimeoutError(f"RPC client was not started after {timeout} seconds")

@retry(stop=stop_after_delay(10), wait=wait_fixed(0.5), reraise=True)
def start_messenger(self, params=[]):
method = "wakuext_startMessenger"
response = self.rpc_request(method, params)
json_response = response.json()

if 'error' in json_response:
assert json_response['error']['code'] == -32000
assert json_response['error']['message'] == "messenger already started"
if "error" in json_response:
assert json_response["error"]["code"] == -32000
assert json_response["error"]["message"] == "messenger already started"
return

self.verify_is_valid_json_rpc_response(response)
Expand All @@ -239,8 +245,7 @@ def get_pubkey(self, display_name):
for account in accounts:
if account.get("name") == display_name:
return account.get("public-key")
raise ValueError(
f"Public key not found for display name: {display_name}")
raise ValueError(f"Public key not found for display name: {display_name}")

def send_contact_request(self, contact_id: str, message: str):
method = "wakuext_sendContactRequest"
Expand Down
5 changes: 3 additions & 2 deletions tests-functional/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import docker
import pytest as pytest

from dataclasses import dataclass

Expand Down Expand Up @@ -43,6 +42,7 @@ def pytest_addoption(parser):
default=None,
)


@dataclass
class Option:
pass
Expand All @@ -55,7 +55,7 @@ def pytest_configure(config):
global option
option = config.option

executor_number = int(os.getenv('EXECUTOR_NUMBER', 5))
executor_number = int(os.getenv("EXECUTOR_NUMBER", 5))
base_port = 7000
range_size = 100

Expand All @@ -66,6 +66,7 @@ def pytest_configure(config):

option.base_dir = os.path.dirname(os.path.abspath(__file__))


def pytest_unconfigure():
docker_client = docker.from_env()
for container_id in option.status_backend_containers:
Expand Down
Loading

0 comments on commit 068248e

Please sign in to comment.