Skip to content

Commit 1795620

Browse files
authored
test_: run functional tests on host (no container) (#6159)
* test_: run on host
1 parent ef177c1 commit 1795620

13 files changed

+194
-144
lines changed

_assets/build/Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ LABEL source="https://github.com/status-im/status-go"
3030
LABEL description="status-go is an underlying part of Status - a browser, messenger, and gateway to a decentralized world."
3131

3232
RUN apk add --no-cache ca-certificates bash libgcc libstdc++ curl
33+
RUN mkdir -p /usr/status-user && chmod -R 777 /usr/status-user
3334
RUN mkdir -p /static/keys
3435
RUN mkdir -p /static/configs
3536

_assets/scripts/run_functional_tests.sh

+25-8
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,35 @@ mkdir -p "${test_results_path}"
2626
all_compose_files="-f ${root_path}/docker-compose.anvil.yml -f ${root_path}/docker-compose.test.status-go.yml"
2727
project_name="status-go-func-tests-$(date +%s)"
2828

29-
export STATUS_BACKEND_COUNT=10
3029
export STATUS_BACKEND_URLS=$(eval echo http://${project_name}-status-backend-{1..${STATUS_BACKEND_COUNT}}:3333 | tr ' ' ,)
3130

32-
# Run functional tests
31+
# Remove orphans
32+
docker ps -a --filter "name=status-go-func-tests-*-status-backend-*" --filter "status=exited" -q | xargs -r docker rm
33+
34+
# Run docker
3335
echo -e "${GRN}Running tests${RST}, HEAD: $(git rev-parse HEAD)"
34-
docker compose -p ${project_name} ${all_compose_files} up -d --build --scale status-backend=${STATUS_BACKEND_COUNT} --remove-orphans
36+
docker compose -p ${project_name} ${all_compose_files} up -d --build --remove-orphans
37+
38+
# Set up virtual environment
39+
venv_path="${root_path}/.venv"
40+
41+
if [[ -d "${venv_path}" ]]; then
42+
echo -e "${GRN}Using existing virtual environment${RST}"
43+
else
44+
echo -e "${GRN}Creating new virtual environment${RST}"
45+
python3 -m venv "${venv_path}"
46+
fi
3547

36-
echo -e "${GRN}Running tests-rpc${RST}" # Follow the logs, wait for them to finish
37-
docker compose -p ${project_name} ${all_compose_files} logs -f tests-rpc > "${root_path}/tests-rpc.log"
48+
source "${venv_path}/bin/activate"
49+
50+
# Upgrade pip and install requirements
51+
echo -e "${GRN}Installing dependencies${RST}"
52+
pip install --upgrade pip
53+
pip install -r "${root_path}/requirements.txt"
54+
55+
# Run functional tests
56+
pytest -m rpc --docker_project_name=${project_name} --codecov_dir=${binary_coverage_reports_path} --junitxml=${test_results_path}/report.xml
57+
exit_code=$?
3858

3959
# Stop containers
4060
echo -e "${GRN}Stopping docker containers${RST}"
@@ -45,9 +65,6 @@ echo -e "${GRN}Saving logs${RST}"
4565
docker compose -p ${project_name} ${all_compose_files} logs status-go > "${root_path}/statusd.log"
4666
docker compose -p ${project_name} ${all_compose_files} logs status-backend > "${root_path}/status-backend.log"
4767

48-
# Retrieve exit code
49-
exit_code=$(docker inspect ${project_name}-tests-rpc-1 -f '{{.State.ExitCode}}');
50-
5168
# Cleanup containers
5269
echo -e "${GRN}Removing docker containers${RST}"
5370
docker compose -p ${project_name} ${all_compose_files} down

tests-functional/README.MD

+5-4
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ Functional tests for status-go
2525
* Status-im contracts will be deployed to the network
2626

2727
### Run tests
28-
- 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 --scale status-backend=10 --remove-orphans`, as result:
29-
* 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`
28+
- 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:
29+
* a container with [status-backend](https://github.com/status-im/status-go/pull/5847) will be created with endpoint exposed on `0.0.0.0:3333`
3030
* status-go will use [anvil](https://book.getfoundry.sh/reference/anvil/) as RPCURL with ChainID 31337
31-
* all Status-im contracts will be deployed to the network
31+
* Status-im contracts will be deployed to the network
3232

33-
* In `./tests-functional/tests` directory run `pytest -m wallet`
33+
* In `./tests-functional/tests` directory run `pytest -m rpc`
34+
* To run tests against binary run `pytest -m <your mark> --url=http:<binary_url>:<binary_port> --user_dir=/<path>`
3435

3536
## Implementation details
3637

tests-functional/clients/status_backend.py

+82-15
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,31 @@
44
import random
55
import threading
66
import requests
7-
from tenacity import retry, stop_after_delay, wait_fixed
7+
import docker
8+
import os
89

10+
from tenacity import retry, stop_after_delay, wait_fixed
911
from clients.signals import SignalClient
1012
from clients.rpc import RpcClient
1113
from datetime import datetime
1214
from conftest import option
13-
from constants import user_1, DEFAULT_DISPLAY_NAME
15+
from constants import user_1, DEFAULT_DISPLAY_NAME, USER_DIR
1416

1517

1618
class StatusBackend(RpcClient, SignalClient):
1719

18-
def __init__(self, await_signals=[], url=None):
19-
try:
20-
url = url if url else random.choice(option.status_backend_urls)
21-
except IndexError:
22-
raise Exception("Not enough status-backend containers, please add more")
23-
option.status_backend_urls.remove(url)
20+
def __init__(self, await_signals=[]):
21+
22+
if option.status_backend_url:
23+
url = option.status_backend_url
24+
else:
25+
self.docker_client = docker.from_env()
26+
host_port = random.choice(option.status_backend_port_range)
27+
28+
self.container = self._start_container(host_port)
29+
url = f"http://127.0.0.1:{host_port}"
30+
option.status_backend_port_range.remove(host_port)
31+
2432

2533
self.api_url = f"{url}/statusgo"
2634
self.ws_url = f"{url}".replace("http", "ws")
@@ -29,14 +37,70 @@ def __init__(self, await_signals=[], url=None):
2937
RpcClient.__init__(self, self.rpc_url)
3038
SignalClient.__init__(self, self.ws_url, await_signals)
3139

40+
self._health_check()
41+
3242
websocket_thread = threading.Thread(target=self._connect)
3343
websocket_thread.daemon = True
3444
websocket_thread.start()
3545

46+
def _start_container(self, host_port):
47+
docker_project_name = option.docker_project_name
48+
49+
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
50+
image_name = f"{docker_project_name}-status-backend:latest"
51+
container_name = f"{docker_project_name}-status-backend-{timestamp}"
52+
53+
coverage_path = option.codecov_dir if option.codecov_dir else os.path.abspath("./coverage/binary")
54+
55+
container_args = {
56+
"image": image_name,
57+
"detach": True,
58+
"name": container_name,
59+
"labels": {"com.docker.compose.project": docker_project_name},
60+
"entrypoint": [
61+
"status-backend",
62+
"--address", "0.0.0.0:3333",
63+
],
64+
"ports": {"3333/tcp": host_port},
65+
"environment": {
66+
"GOCOVERDIR": "/coverage/binary",
67+
},
68+
"volumes": {
69+
coverage_path: {
70+
"bind": "/coverage/binary",
71+
"mode": "rw",
72+
}
73+
},
74+
}
75+
76+
if "FUNCTIONAL_TESTS_DOCKER_UID" in os.environ:
77+
container_args["user"] = os.environ["FUNCTIONAL_TESTS_DOCKER_UID"]
78+
79+
container = self.docker_client.containers.run(**container_args)
80+
81+
network = self.docker_client.networks.get(
82+
f"{docker_project_name}_default")
83+
network.connect(container)
84+
85+
option.status_backend_containers.append(container.id)
86+
return container
87+
88+
def _health_check(self):
89+
start_time = time.time()
90+
while True:
91+
try:
92+
self.api_valid_request(method="Fleets", data=[])
93+
break
94+
except Exception as e:
95+
if time.time() - start_time > 20:
96+
raise Exception(e)
97+
time.sleep(1)
98+
3699
def api_request(self, method, data, url=None):
37100
url = url if url else self.api_url
38101
url = f"{url}/{method}"
39-
logging.info(f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
102+
logging.info(
103+
f"Sending POST request to url {url} with data: {json.dumps(data, sort_keys=True, indent=4)}")
40104
response = requests.post(url, json=data)
41105
logging.info(f"Got response: {response.content}")
42106
return response
@@ -46,7 +110,8 @@ def verify_is_valid_api_response(self, response):
46110
assert response.content
47111
logging.info(f"Got response: {response.content}")
48112
try:
49-
assert not response.json()["error"]
113+
error = response.json()["error"]
114+
assert not error, f"Error: {error}"
50115
except json.JSONDecodeError:
51116
raise AssertionError(
52117
f"Invalid JSON in response: {response.content}")
@@ -58,7 +123,7 @@ def api_valid_request(self, method, data):
58123
self.verify_is_valid_api_response(response)
59124
return response
60125

61-
def init_status_backend(self, data_dir="/"):
126+
def init_status_backend(self, data_dir=USER_DIR):
62127
method = "InitializeApplication"
63128
data = {
64129
"dataDir": data_dir,
@@ -68,7 +133,7 @@ def init_status_backend(self, data_dir="/"):
68133
}
69134
return self.api_valid_request(method, data)
70135

71-
def create_account_and_login(self, data_dir="/", display_name=DEFAULT_DISPLAY_NAME, password=user_1.password):
136+
def create_account_and_login(self, data_dir=USER_DIR, display_name=DEFAULT_DISPLAY_NAME, password=user_1.password):
72137
method = "CreateAccountAndLogin"
73138
data = {
74139
"rootDataDir": data_dir,
@@ -81,7 +146,7 @@ def create_account_and_login(self, data_dir="/", display_name=DEFAULT_DISPLAY_NA
81146
}
82147
return self.api_valid_request(method, data)
83148

84-
def restore_account_and_login(self, data_dir="/",display_name=DEFAULT_DISPLAY_NAME, user=user_1,
149+
def restore_account_and_login(self, data_dir=USER_DIR, display_name=DEFAULT_DISPLAY_NAME, user=user_1,
85150
network_id=31337):
86151
method = "RestoreAccountAndLogin"
87152
data = {
@@ -136,7 +201,8 @@ def restore_account_and_wait_for_rpc_client_to_start(self, timeout=60):
136201
return
137202
except AssertionError:
138203
time.sleep(3)
139-
raise TimeoutError(f"RPC client was not started after {timeout} seconds")
204+
raise TimeoutError(
205+
f"RPC client was not started after {timeout} seconds")
140206

141207
@retry(stop=stop_after_delay(10), wait=wait_fixed(0.5), reraise=True)
142208
def start_messenger(self, params=[]):
@@ -173,7 +239,8 @@ def get_pubkey(self, display_name):
173239
for account in accounts:
174240
if account.get("name") == display_name:
175241
return account.get("public-key")
176-
raise ValueError(f"Public key not found for display name: {display_name}")
242+
raise ValueError(
243+
f"Public key not found for display name: {display_name}")
177244

178245
def send_contact_request(self, params=[]):
179246
method = "wakuext_sendContactRequest"

tests-functional/conftest.py

+39-19
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,46 @@
11
import os
2-
import threading
3-
from dataclasses import dataclass
4-
2+
import docker
53
import pytest as pytest
64

5+
from dataclasses import dataclass
6+
77

88
def pytest_addoption(parser):
99
parser.addoption(
10-
"--rpc_url_statusd",
10+
"--status_backend_url",
1111
action="store",
1212
help="",
13-
default="http://0.0.0.0:3333",
13+
default=None,
1414
)
1515
parser.addoption(
16-
"--ws_url_statusd",
16+
"--anvil_url",
1717
action="store",
1818
help="",
19-
default="ws://0.0.0.0:8354",
19+
default="http://0.0.0.0:8545",
2020
)
2121
parser.addoption(
22-
"--status_backend_urls",
22+
"--password",
2323
action="store",
2424
help="",
25-
default=[
26-
f"http://0.0.0.0:{3314 + i}" for i in range(
27-
int(os.getenv("STATUS_BACKEND_COUNT", 10))
28-
)
29-
],
25+
default="Strong12345",
3026
)
3127
parser.addoption(
32-
"--anvil_url",
28+
"--docker_project_name",
3329
action="store",
3430
help="",
35-
default="http://0.0.0.0:8545",
31+
default="tests-functional",
3632
)
3733
parser.addoption(
38-
"--password",
34+
"--codecov_dir",
3935
action="store",
4036
help="",
41-
default="Strong12345",
37+
default=None,
38+
)
39+
parser.addoption(
40+
"--user_dir",
41+
action="store",
42+
help="",
43+
default=None,
4244
)
4345

4446
@dataclass
@@ -52,6 +54,24 @@ class Option:
5254
def pytest_configure(config):
5355
global option
5456
option = config.option
55-
if type(option.status_backend_urls) is str:
56-
option.status_backend_urls = option.status_backend_urls.split(",")
57+
58+
executor_number = int(os.getenv('EXECUTOR_NUMBER', 5))
59+
base_port = 7000
60+
range_size = 100
61+
62+
start_port = base_port + (executor_number * range_size)
63+
64+
option.status_backend_port_range = list(range(start_port, start_port + range_size - 1))
65+
option.status_backend_containers = []
66+
5767
option.base_dir = os.path.dirname(os.path.abspath(__file__))
68+
69+
def pytest_unconfigure():
70+
docker_client = docker.from_env()
71+
for container_id in option.status_backend_containers:
72+
try:
73+
container = docker_client.containers.get(container_id)
74+
container.stop(timeout=30)
75+
container.remove()
76+
except Exception as e:
77+
print(e)

tests-functional/constants.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from dataclasses import dataclass
2+
from conftest import option
23
import os
34

45

@@ -26,4 +27,5 @@ class Account:
2627
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../"))
2728
TESTS_DIR = os.path.join(PROJECT_ROOT, "tests-functional")
2829
SIGNALS_DIR = os.path.join(TESTS_DIR, "signals")
29-
LOG_SIGNALS_TO_FILE = False # used for debugging purposes
30+
LOG_SIGNALS_TO_FILE = False # used for debugging purposes
31+
USER_DIR = option.user_dir if option.user_dir else "/usr/status-user"

tests-functional/docker-compose.status-go.local.yml

-4
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@ services:
22
anvil:
33
ports:
44
- 8545:8545
5-
status-go:
6-
ports:
7-
- 3333:3333
8-
- 8354:8354
95
status-backend:
106
ports:
117
- 3314-3324:3333

0 commit comments

Comments
 (0)