From f5c647b47bf3e360846ecc24cd4a2ba355294666 Mon Sep 17 00:00:00 2001 From: protolambda Date: Tue, 18 May 2021 16:08:30 +0200 Subject: [PATCH] switch configuration to named tuple for reliable hashing, add test for config override functionality --- setup.py | 4 +- .../altair/unittests/test_config_override.py | 19 ++++++++++ tests/core/pyspec/eth2spec/test/context.py | 37 +++++++++++++++---- .../pyspec/eth2spec/utils/hash_function.py | 6 ++- 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 tests/core/pyspec/eth2spec/test/altair/unittests/test_config_override.py diff --git a/setup.py b/setup.py index 070003ee9..d006f5e54 100644 --- a/setup.py +++ b/setup.py @@ -326,7 +326,7 @@ from dataclasses import ( field, ) from typing import ( - Any, Callable, Dict, Set, Sequence, Tuple, Optional, TypeVar + Any, Callable, Dict, Set, Sequence, Tuple, Optional, TypeVar, NamedTuple ) from eth2spec.utils.ssz.ssz_impl import hash_tree_root, copy, uint_to_bytes @@ -603,7 +603,7 @@ def objects_to_spec(preset_name: str, out += f' # {vardef.comment}' return out - config_spec = '@dataclass\nclass Configuration(object):\n' + config_spec = 'class Configuration(NamedTuple):\n' config_spec += ' PRESET_BASE: str\n' config_spec += '\n'.join(f' {k}: {v.type_name if v.type_name is not None else "int"}' for k, v in spec_object.config_vars.items()) diff --git a/tests/core/pyspec/eth2spec/test/altair/unittests/test_config_override.py b/tests/core/pyspec/eth2spec/test/altair/unittests/test_config_override.py new file mode 100644 index 000000000..f1503c39f --- /dev/null +++ b/tests/core/pyspec/eth2spec/test/altair/unittests/test_config_override.py @@ -0,0 +1,19 @@ +from eth2spec.test.context import spec_configured_state_test, with_phases +from eth2spec.test.helpers.constants import ALTAIR + + +@with_phases([ALTAIR]) +@spec_configured_state_test({ + 'GENESIS_FORK_VERSION': '0x12345678', + 'ALTAIR_FORK_VERSION': '0x11111111', + 'ALTAIR_FORK_EPOCH': 4 +}) +def test_config_override(spec, state): + assert spec.config.ALTAIR_FORK_EPOCH == 4 + assert spec.config.GENESIS_FORK_VERSION != spec.Version('0x00000000') + assert spec.config.GENESIS_FORK_VERSION == spec.Version('0x12345678') + assert spec.config.ALTAIR_FORK_VERSION == spec.Version('0x11111111') + assert state.fork.current_version == spec.Version('0x11111111') + # TODO: it would be nice if the create_genesis_state actually outputs a state + # for the fork with a slot that matches at least the fork boundary. + # assert spec.get_current_epoch(state) >= 4 diff --git a/tests/core/pyspec/eth2spec/test/context.py b/tests/core/pyspec/eth2spec/test/context.py index 4a3c9acb5..7310e831b 100644 --- a/tests/core/pyspec/eth2spec/test/context.py +++ b/tests/core/pyspec/eth2spec/test/context.py @@ -23,9 +23,13 @@ from lru import LRU # TODO: currently phases are defined as python modules. # It would be better if they would be more well-defined interfaces for stronger typing. +class Configuration(Protocol): + PRESET_BASE: str + class Spec(Protocol): - version: str + fork: str + config: Configuration class SpecPhase0(Spec): @@ -78,8 +82,8 @@ def with_custom_state(balances_fn: Callable[[Any], Sequence[int]], def deco(fn): def entry(*args, spec: Spec, phases: SpecForks, **kw): - # make a key for the state - key = (spec.fork, spec.config.PRESET_BASE, spec.__file__, balances_fn, threshold_fn) + # make a key for the state, unique to the fork + config (incl preset choice) and balances/activations + key = (spec.fork, spec.config.__hash__(), spec.__file__, balances_fn, threshold_fn) global _custom_state_cache_dict if key not in _custom_state_cache_dict: state = _prepare_state(balances_fn, threshold_fn, spec, phases) @@ -211,6 +215,14 @@ def spec_state_test(fn): return spec_test(with_state(single_phase(fn))) +def spec_configured_state_test(conf): + overrides = with_config_overrides(conf) + + def decorator(fn): + return spec_test(overrides(with_state(single_phase(fn)))) + return decorator + + def expect_assertion_error(fn): bad = False try: @@ -380,9 +392,11 @@ def with_presets(preset_bases, reason=None): def with_config_overrides(config_overrides): """ - Decorator that applies a dict of config value overrides to the spec during execution. - This may be slow due to having to reload the spec modules, - since the specs uses globals instead of a configuration object. + WARNING: the spec_test decorator must wrap this, to ensure the decorated test actually runs. + This decorator forces the test to yield, and pytest doesn't run generator tests, and instead silently passes it. + Use 'spec_configured_state_test' instead of 'spec_state_test' if you are unsure. + + This is a decorator that applies a dict of config value overrides to the spec during execution. """ def decorator(fn): def wrapper(*args, spec: Spec, **kw): @@ -390,9 +404,16 @@ def with_config_overrides(config_overrides): old_config = spec.config # apply our overrides to a copy of it, and apply it to the spec - tmp_config = deepcopy(old_config) + tmp_config = deepcopy(old_config._asdict()) # not a private method, there are multiple tmp_config.update(config_overrides) - spec.config = tmp_config + config_types = spec.Configuration.__annotations__ + # Retain types of all config values + test_config = {k: config_types[k](v) for k, v in tmp_config.items()} + + # Output the config for test vectors (TODO: check config YAML encoding) + yield 'config', test_config + + spec.config = spec.Configuration(**test_config) # Run the function out = fn(*args, spec=spec, **kw) diff --git a/tests/core/pyspec/eth2spec/utils/hash_function.py b/tests/core/pyspec/eth2spec/utils/hash_function.py index 470cb1da9..2b68ab023 100644 --- a/tests/core/pyspec/eth2spec/utils/hash_function.py +++ b/tests/core/pyspec/eth2spec/utils/hash_function.py @@ -6,4 +6,8 @@ ZERO_BYTES32 = b'\x00' * 32 def hash(x: Union[bytes, bytearray, memoryview]) -> Bytes32: - return Bytes32(sha256(x).digest()) + try: + return Bytes32(sha256(x).digest()) + except TypeError: + print(x) + raise Exception("bad")