Skip to content

Commit 1a3d64f

Browse files
authored
Re-organize the fixture filler helper tools (#1219)
* Re-organize the fixture filler helper tools * cleanup trinity module * move rlp util to be not private * fix docs
1 parent 0c12ccf commit 1a3d64f

40 files changed

+1688
-1454
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ docs/modules.rst
4646
.pytest_cache/
4747

4848
# fixtures
49-
fixtures/**
49+
./fixtures/**
5050

5151
# profiling
5252
prof/**

docs/api/eth/api.tools.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Tools
2+
=====
3+
4+
5+
.. toctree::
6+
:maxdepth: 4
7+
:name: toc-eth-api-tools
8+
:caption: Tools
9+
10+
tools/api.tools.fixtures

docs/api/eth/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ This section aims to provide a detailed description of all APIs. If you are look
88
:name: toc-api-eth
99
:caption: API
1010

11+
1112
api.chain
1213
api.db
1314
api.exceptions
1415
api.rlp
16+
api.tools
1517
api.vm
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
Builder Tools
2+
=============
3+
4+
5+
The JSON test fillers found in `eth.tools.fixtures` is a set of tools which facilitate
6+
creating standard JSON consensus tests as found in the
7+
`ethereum/tests repository <https://github.com/ethereum/tests>`_.
8+
9+
.. note:: Only VM and state tests are supported right now.
10+
11+
12+
State Test Fillers
13+
------------------
14+
15+
Tests are generated in two steps.
16+
17+
* First, a *test filler* is written that contains a high level description of the test case.
18+
* Subsequently, the filler is compiled to the actual test in a process called
19+
filling, mainly consisting of calculating the resulting state root.
20+
21+
The test builder represents each stage as a nested dictionary. Helper functions are provided to
22+
assemble the filler file step by step in the correct format. The
23+
:func:`~eth.tools.fixtures.fillers.fill_test` function handles compilation and
24+
takes additional parameters that can't be inferred from the filler.
25+
26+
27+
Creating a Filler
28+
~~~~~~~~~~~~~~~~~
29+
30+
Fillers are generated in a functional fashion by piping a dictionary through a
31+
sequence of functions.
32+
33+
.. code-block:: python
34+
35+
filler = pipe(
36+
setup_main_filler("test"),
37+
pre_state(
38+
(sender, "balance", 1),
39+
(receiver, "balance", 0),
40+
),
41+
expect(
42+
networks=["Frontier"],
43+
transaction={
44+
"to": receiver,
45+
"value": 1,
46+
"secretKey": sender_key,
47+
},
48+
post_state=[
49+
[sender, "balance", 0],
50+
[receiver, "balance", 1],
51+
]
52+
)
53+
)
54+
55+
.. note::
56+
57+
Note that :func:`~eth.tools.fixtures.setup_filler` returns a
58+
dictionary, whereas all of the following functions such as
59+
:func:`~eth.tools.fixtures.pre_state`,
60+
:func:`~eth.tools.fixtures.expect`, expect to be passed a dictionary
61+
as their single argument and return an updated version of the dictionary.
62+
63+
64+
.. autofunction:: eth.tools.fixtures.fillers.common.setup_main_filler
65+
66+
This function kicks off the filler generation process by creating the general filler scaffold with
67+
a test name and general information about the testing environment.
68+
69+
For tests for the main chain, the `environment` parameter is expected to be a dictionary with some
70+
or all of the following keys:
71+
72+
+------------------------+---------------------------------+
73+
| key | description |
74+
+========================+=================================+
75+
| ``"currentCoinbase"`` | the coinbase address |
76+
+------------------------+---------------------------------+
77+
| ``"currentNumber"`` | the block number |
78+
+------------------------+---------------------------------+
79+
| ``"previousHash"`` | the hash of the parent block |
80+
+------------------------+---------------------------------+
81+
| ``"currentDifficulty"``| the block's difficulty |
82+
+------------------------+---------------------------------+
83+
| ``"currentGasLimit"`` | the block's gas limit |
84+
+------------------------+---------------------------------+
85+
| ``"currentTimestamp"`` | the timestamp of the block |
86+
+------------------------+---------------------------------+
87+
88+
89+
.. autofunction:: eth.tools.fixtures.fillers.pre_state
90+
91+
This function specifies the state prior to the test execution. Multiple invocations don't override
92+
the state but extend it instead.
93+
94+
In general, the elements of `state_definitions` are nested dictionaries of the following form:
95+
96+
.. code-block:: python
97+
98+
{
99+
address: {
100+
"nonce": <account nonce>,
101+
"balance": <account balance>,
102+
"code": <account code>,
103+
"storage": {
104+
<storage slot>: <storage value>
105+
}
106+
}
107+
}
108+
109+
To avoid unnecessary nesting especially if only few fields per account are specified, the following
110+
and similar formats are possible as well:
111+
112+
.. code-block:: python
113+
114+
(address, "balance", <account balance>)
115+
(address, "storage", <storage slot>, <storage value>)
116+
(address, "storage", {<storage slot>: <storage value>})
117+
(address, {"balance", <account balance>})
118+
119+
120+
.. autofunction:: eth.tools.fixtures.fillers.execution
121+
122+
For VM tests, this function specifies the code that is being run as well as the current state of
123+
the EVM. State tests don't support this object. The parameter is a dictionary specifying some or
124+
all of the following keys:
125+
126+
+--------------------+------------------------------------------------------------+
127+
| key | description |
128+
+====================+============================================================+
129+
| ``"address"`` | the address of the account executing the code |
130+
+--------------------+------------------------------------------------------------+
131+
| ``"caller"`` | the caller address |
132+
+--------------------+------------------------------------------------------------+
133+
| ``"origin"`` | the origin address (defaulting to the caller address) |
134+
+--------------------+------------------------------------------------------------+
135+
| ``"value"`` | the value of the call |
136+
+--------------------+------------------------------------------------------------+
137+
| ``"data"`` | the data passed with the call |
138+
+--------------------+------------------------------------------------------------+
139+
| ``"gasPrice"`` | the gas price of the call |
140+
+--------------------+------------------------------------------------------------+
141+
| ``"gas"`` | the amount of gas allocated for the call |
142+
+--------------------+------------------------------------------------------------+
143+
| ``"code"`` | the bytecode to execute |
144+
+--------------------+------------------------------------------------------------+
145+
| ``"vyperLLLCode"`` | the code in Vyper LLL (compiled to bytecode automatically) |
146+
+--------------------+------------------------------------------------------------+
147+
148+
149+
.. autofunction:: eth.tools.fixtures.fillers.expect
150+
151+
This specifies the expected result of the test.
152+
153+
For state tests, multiple expectations can be given, differing in the transaction data, gas
154+
limit, and value, in the applicable networks, and as a result also in the post state. VM tests
155+
support only a single expectation with no specified network and no transaction (here, its role is
156+
played by :func:`~eth.tools.fixtures.fillers.execution`).
157+
158+
* ``post_state`` is a list of state definition in the same form as expected by `pre_state`. State items
159+
that are not set explicitly default to their pre state.
160+
161+
* ``networks`` defines the forks under which the expectation is applicable. It should be a sublist of
162+
the following identifiers (also available in `ALL_FORKS`):
163+
164+
* ``"Frontier"``
165+
* ``"Homestead"``
166+
* ``"EIP150"``
167+
* ``"EIP158"``
168+
* ``"Byzantium"``
169+
170+
* ``transaction`` is a dictionary coming in two variants. For the main shard:
171+
172+
+----------------+-------------------------------+
173+
| key | description |
174+
+================+===============================+
175+
| ``"data"`` | the transaction data, |
176+
+----------------+-------------------------------+
177+
| ``"gasLimit"`` | the transaction gas limit, |
178+
+----------------+-------------------------------+
179+
| ``"gasPrice"`` | the gas price, |
180+
+----------------+-------------------------------+
181+
| ``"nonce"`` | the transaction nonce, |
182+
+----------------+-------------------------------+
183+
| ``"value"`` | the transaction value |
184+
+----------------+-------------------------------+
185+
186+
In addition, one should specify either the signature itself (via keys ``"v"``, ``"r"``, and ``"s"``) or
187+
a private key used for signing (via ``"secretKey"``).

eth/chains/base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
encode_hex,
9696
)
9797
from eth.utils.rlp import (
98-
ensure_imported_block_unchanged,
98+
validate_imported_block_unchanged,
9999
)
100100

101101
if TYPE_CHECKING:
@@ -635,7 +635,7 @@ def import_block(self,
635635

636636
# Validate the imported block.
637637
if perform_validation:
638-
ensure_imported_block_unchanged(imported_block, block)
638+
validate_imported_block_unchanged(imported_block, block)
639639
self.validate_block(imported_block)
640640

641641
(
File renamed without changes.

eth/tools/_utils/git.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import subprocess
2+
3+
from eth_utils import to_text
4+
5+
6+
def get_version_from_git():
7+
version = subprocess.check_output(["git", "describe"]).strip()
8+
return to_text(version)

eth/tools/_utils/hashing.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from eth_hash.auto import keccak
2+
3+
import rlp
4+
5+
from eth.rlp.logs import Log
6+
7+
8+
def hash_log_entries(log_entries):
9+
"""
10+
Helper function for computing the RLP hash of the logs from transaction
11+
execution.
12+
"""
13+
logs = [Log(*entry) for entry in log_entries]
14+
encoded_logs = rlp.encode(logs)
15+
logs_hash = keccak(encoded_logs)
16+
return logs_hash

eth/tools/_utils/mappings.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from collections.abc import Mapping
2+
import itertools
3+
4+
from cytoolz import merge_with
5+
6+
7+
def merge_if_dicts(values):
8+
if all(isinstance(item, Mapping) for item in values):
9+
return merge_with(merge_if_dicts, *values)
10+
else:
11+
return values[-1]
12+
13+
14+
def deep_merge(*dicts):
15+
return merge_with(merge_if_dicts, *dicts)
16+
17+
18+
def is_cleanly_mergable(*dicts):
19+
"""Check that nothing will be overwritten when dictionaries are merged using `deep_merge`.
20+
21+
Examples:
22+
23+
>>> is_cleanly_mergable({"a": 1}, {"b": 2}, {"c": 3})
24+
True
25+
>>> is_cleanly_mergable({"a": 1}, {"b": 2}, {"a": 0, c": 3})
26+
False
27+
>>> is_cleanly_mergable({"a": 1, "b": {"ba": 2}}, {"c": 3, {"b": {"bb": 4}})
28+
True
29+
>>> is_cleanly_mergable({"a": 1, "b": {"ba": 2}}, {"b": {"ba": 4}})
30+
False
31+
32+
"""
33+
if len(dicts) <= 1:
34+
return True
35+
elif len(dicts) == 2:
36+
if not all(isinstance(d, Mapping) for d in dicts):
37+
return False
38+
else:
39+
shared_keys = set(dicts[0].keys()) & set(dicts[1].keys())
40+
return all(is_cleanly_mergable(dicts[0][key], dicts[1][key]) for key in shared_keys)
41+
else:
42+
dict_combinations = itertools.combinations(dicts, 2)
43+
return all(is_cleanly_mergable(*combination) for combination in dict_combinations)

eth/tools/_utils/vyper.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import functools
2+
3+
try:
4+
from vyper.compile_lll import (
5+
compile_to_assembly,
6+
assembly_to_evm,
7+
)
8+
from vyper.parser.parser_utils import LLLnode
9+
except ImportError:
10+
vyper_available = False
11+
else:
12+
vyper_available = True
13+
14+
15+
def require_vyper(fn):
16+
@functools.wraps(fn)
17+
def inner(*args, **kwargs):
18+
if vyper_available:
19+
return fn(*args, **kwargs)
20+
else:
21+
raise ImportError("The `{0}` function requires the vyper compiler.")
22+
return inner
23+
24+
25+
@require_vyper
26+
def compile_vyper_lll(vyper_code):
27+
lll_node = LLLnode.from_list(vyper_code)
28+
assembly = compile_to_assembly(lll_node)
29+
code = assembly_to_evm(assembly)
30+
return code

0 commit comments

Comments
 (0)