Skip to content

Commit df41b19

Browse files
committed
Add integration tests against geth and aleth
1 parent 7128bb0 commit df41b19

File tree

7 files changed

+192
-1
lines changed

7 files changed

+192
-1
lines changed

.circleci/config.yml

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
lint:
4646
<<: *common
4747
docker:
48-
- image: circleci/python:3.6
48+
- image: circleci/python:3.7
4949
environment:
5050
TOXENV: lint
5151
py36-core:
@@ -66,6 +66,37 @@ jobs:
6666
- image: pypy
6767
environment:
6868
TOXENV: pypy3-core
69+
# Integration tests use docker to spin up different backends (geth, aleth etc.). Running
70+
# docker is simpler when the host isn't a docker container itself
71+
py36-integration-geth:
72+
<<: *common
73+
machine:
74+
image: ubuntu-1604:201903-01
75+
environment:
76+
TOXENV: py36-integration-geth
77+
PYENV_VERSION: 3.6.5
78+
py36-integration-aleth:
79+
<<: *common
80+
machine:
81+
image: ubuntu-1604:201903-01
82+
environment:
83+
TOXENV: py36-integration-aleth
84+
PYENV_VERSION: 3.6.5
85+
py37-integration-geth:
86+
<<: *common
87+
machine:
88+
image: ubuntu-1604:201903-01
89+
environment:
90+
TOXENV: py37-integration-geth
91+
PYENV_VERSION: 3.7.0
92+
py37-integration-aleth:
93+
<<: *common
94+
machine:
95+
image: ubuntu-1604:201903-01
96+
environment:
97+
TOXENV: py37-integration-aleth
98+
PYENV_VERSION: 3.7.0
99+
69100
workflows:
70101
version: 2
71102
test:
@@ -75,3 +106,7 @@ workflows:
75106
- py36-core
76107
- py37-core
77108
- pypy3-core
109+
- py36-integration-geth
110+
- py36-integration-aleth
111+
- py37-integration-geth
112+
- py37-integration-aleth

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ chains
5656

5757
# tox/pytest cache
5858
.cache
59+
.pytest_cache
5960

6061
# Test output logs
6162
logs
File renamed without changes.

dopple/tools/runner.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import logging
2+
import pathlib
3+
import socket
4+
import tempfile
5+
import time
6+
from typing import AsyncIterator, Callable
7+
8+
import trio
9+
10+
try:
11+
from contextlib import asynccontextmanager
12+
except ImportError:
13+
# We use this as a fallback to support Python 3.6
14+
from async_generator import asynccontextmanager # type: ignore
15+
16+
17+
DOPPLE_FILE = pathlib.Path(__file__).parent.parent / "dopple.py"
18+
19+
20+
@asynccontextmanager
21+
async def run_eth_client_in_docker(
22+
generate_cmd_fn: Callable[[pathlib.Path], str],
23+
generate_ipc_path_fn: Callable[[pathlib.Path], pathlib.Path],
24+
) -> AsyncIterator[pathlib.Path]:
25+
"""
26+
Create an ``asynccontextmanager`` that runs an Ethereum client within docker
27+
under the user account of the host user. A temporary directory is created on
28+
the host system and should be mapped into the docker container to allow the
29+
client to share IPC files with the host system. Yield the IPC path of the client.
30+
31+
:param generate_cmd_fn: A function taking the ``Path`` of the temporary directory
32+
and generating the relevant part of the docker command to launch the client with
33+
the mapped directory.
34+
35+
:param generate_ipc_path_fn: A function taking the ``Path`` of the temprorary directory
36+
and generating the ``Path`` of the IPC file that dopple can connect to.
37+
"""
38+
with tempfile.TemporaryDirectory() as tmpdir:
39+
temp_dir = pathlib.Path(tmpdir)
40+
41+
start_cmd = (
42+
f"docker run --rm --name {temp_dir.name} "
43+
f"--user $(id -u):$(id -g) {generate_cmd_fn(temp_dir)}"
44+
)
45+
stop_cmd = f"docker stop {temp_dir.name}"
46+
47+
ipc_path = generate_ipc_path_fn(temp_dir)
48+
logging.debug(f"Starting client, ipc path: {ipc_path}")
49+
async with await trio.open_process(start_cmd, shell=True):
50+
logging.debug("Started client, waiting for IPC socket to be ready")
51+
wait_for_socket(ipc_path)
52+
logging.debug("IPC ready")
53+
yield ipc_path
54+
logging.debug("Killing client")
55+
await trio.run_process(stop_cmd, shell=True)
56+
logging.debug("Killed client")
57+
58+
59+
@asynccontextmanager
60+
async def run_dopple_as_script(ipc_path: pathlib.Path) -> AsyncIterator[None]:
61+
"""
62+
Run geth, then run dopple as a script file to connect to it.
63+
"""
64+
logging.debug("Starting dopple")
65+
async with await trio.open_process([DOPPLE_FILE, ipc_path]) as proc:
66+
logging.debug("Started dopple")
67+
await trio.sleep(0.5)
68+
yield
69+
logging.debug("Terminating dopple")
70+
proc.terminate()
71+
await proc.wait()
72+
logging.debug("Terminated dopple")
73+
74+
75+
def wait_for_socket(ipc_path: pathlib.Path, timeout: int = 10) -> None:
76+
"""
77+
Wait for the ``ipc_path`` to be ready.
78+
"""
79+
start = time.monotonic()
80+
while time.monotonic() < start + timeout:
81+
try:
82+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
83+
sock.connect(str(ipc_path))
84+
sock.settimeout(timeout)
85+
except (FileNotFoundError, socket.error):
86+
time.sleep(0.01)
87+
else:
88+
break

setup.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
extras_require = {
99
'test': [
1010
"pytest>=5.1.3,<6",
11+
"pytest-trio==0.5.2",
1112
"pytest-xdist==1.18.1",
13+
"requests==2.22.0",
1214
"tox>=2.9.1,<3",
15+
"trio==0.13.0",
1316
],
1417
'lint': [
1518
"black==19.3b",
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from dopple.tools.runner import run_dopple_as_script, run_eth_client_in_docker
2+
import pytest
3+
import requests
4+
5+
# The following tests invoke dopple as a pure file (as opposed to a package installed through pip)
6+
# and test it's functionality against different clients.
7+
8+
9+
def geth_cmd(tmpdir):
10+
return (
11+
f"-v {tmpdir}:/tmp/.ethereum ethereum/client-go:v1.9.6 --datadir /tmp/.ethereum"
12+
)
13+
14+
15+
def generate_geth_ipc_path(tmpdir):
16+
return tmpdir / "geth.ipc"
17+
18+
19+
def aleth_cmd(tmpdir):
20+
return f"-v {tmpdir}:/.ethereum ethereum/aleth:1.6.0"
21+
22+
23+
GETH_LAUNCH_FNS = geth_cmd, generate_geth_ipc_path
24+
# Not a bug, aleth names its IPC file `geth.ipc`
25+
ALETH_LAUNCH_FNS = aleth_cmd, generate_geth_ipc_path
26+
27+
# Test cases *must* follow this order to label each test case to the correct client
28+
TEST_IDS = ("geth", "aleth")
29+
30+
31+
@pytest.mark.parametrize(
32+
"client_config", (GETH_LAUNCH_FNS, ALETH_LAUNCH_FNS), ids=TEST_IDS
33+
)
34+
@pytest.mark.trio
35+
async def test_run_and_get_info(client_config):
36+
async with run_eth_client_in_docker(*client_config) as ipc_path:
37+
async with run_dopple_as_script(ipc_path):
38+
ret = requests.get("http://127.0.0.1:8545")
39+
assert str(ipc_path) in ret.text
40+
assert "connected: True" in ret.text
41+
42+
43+
@pytest.mark.parametrize(
44+
"client_config", (GETH_LAUNCH_FNS, ALETH_LAUNCH_FNS), ids=TEST_IDS
45+
)
46+
@pytest.mark.trio
47+
async def test_can_request_data(client_config):
48+
data = {
49+
"jsonrpc": "2.0",
50+
"method": "eth_getBlockByNumber",
51+
"params": ["0x0", True],
52+
"id": 1,
53+
}
54+
async with run_eth_client_in_docker(*client_config) as ipc_path:
55+
async with run_dopple_as_script(ipc_path):
56+
ret = requests.post("http://127.0.0.1:8545", json=data)
57+
# Assert the genesis block hash is in the result
58+
assert (
59+
"0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
60+
in ret.text
61+
)

tox.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
[tox]
22
envlist=
33
py{36,37,py3}-core
4+
py{36,37}-integration-{geth, aleth}
45
lint
56
docs
67

@@ -22,6 +23,8 @@ ignore=
2223
usedevelop=True
2324
commands=
2425
core: pytest {posargs:tests/core}
26+
integration-geth: pytest {posargs:tests/integration -k 'geth'}
27+
integration-aleth: pytest {posargs:tests/integration -k 'aleth'}
2528
docs: make build-docs
2629
basepython =
2730
docs: python

0 commit comments

Comments
 (0)