Testing with Ethereum Tester

Ethereum Tester is a tool suite for testing Ethereum based applications.

This section provides a quick overview of testing with eth-tester. To learn more, you can view the documentation at the Github repo or join the Gitter channel.

Getting Started

Prior to testing, the Vyper specific contract conversion and the blockchain related fixtures need to be set up. These fixtures will be used in every test file and should therefore be defined in conftest.py.

Note

Since the testing is done in the pytest framework, you can make use of pytest.ini, tox.ini and setup.cfg and you can use most IDEs’ pytest plugins.

conftest.py
  1import json
  2import logging
  3from contextlib import contextmanager
  4from functools import wraps
  5
  6import hypothesis
  7import pytest
  8import web3.exceptions
  9from eth_tester import EthereumTester, PyEVMBackend
 10from eth_tester.exceptions import TransactionFailed
 11from eth_utils import setup_DEBUG2_logging
 12from eth_utils.toolz import compose
 13from hexbytes import HexBytes
 14from web3 import Web3
 15from web3.contract import Contract
 16from web3.providers.eth_tester import EthereumTesterProvider
 17
 18import vyper.evm.opcodes as evm
 19from tests.utils import working_directory
 20from vyper import compiler
 21from vyper.ast.grammar import parse_vyper_source
 22from vyper.codegen.ir_node import IRnode
 23from vyper.compiler.input_bundle import FilesystemInputBundle, InputBundle
 24from vyper.compiler.settings import OptimizationLevel, Settings, _set_debug_mode
 25from vyper.ir import compile_ir, optimizer
 26from vyper.utils import ERC5202_PREFIX
 27
 28# Import the base fixtures
 29pytest_plugins = ["tests.fixtures.memorymock"]
 30
 31############
 32# PATCHING #
 33############
 34
 35
 36# disable hypothesis deadline globally
 37hypothesis.settings.register_profile("ci", deadline=None)
 38hypothesis.settings.load_profile("ci")
 39
 40
 41def set_evm_verbose_logging():
 42    logger = logging.getLogger("eth.vm.computation.BaseComputation")
 43    setup_DEBUG2_logging()
 44    logger.setLevel("DEBUG2")
 45
 46
 47# Useful options to comment out whilst working:
 48# set_evm_verbose_logging()
 49#
 50# from vdb import vdb
 51# vdb.set_evm_opcode_debugger()
 52
 53
 54def pytest_addoption(parser):
 55    parser.addoption(
 56        "--optimize",
 57        choices=["codesize", "gas", "none"],
 58        default="gas",
 59        help="change optimization mode",
 60    )
 61    parser.addoption("--enable-compiler-debug-mode", action="store_true")
 62
 63    parser.addoption(
 64        "--evm-version",
 65        choices=list(evm.EVM_VERSIONS.keys()),
 66        default="shanghai",
 67        help="set evm version",
 68    )
 69
 70
 71@pytest.fixture(scope="module")
 72def output_formats():
 73    output_formats = compiler.OUTPUT_FORMATS.copy()
 74    del output_formats["bb"]
 75    del output_formats["bb_runtime"]
 76    return output_formats
 77
 78
 79@pytest.fixture(scope="module")
 80def optimize(pytestconfig):
 81    flag = pytestconfig.getoption("optimize")
 82    return OptimizationLevel.from_string(flag)
 83
 84
 85@pytest.fixture(scope="session", autouse=True)
 86def debug(pytestconfig):
 87    debug = pytestconfig.getoption("enable_compiler_debug_mode")
 88    assert isinstance(debug, bool)
 89    _set_debug_mode(debug)
 90
 91
 92@pytest.fixture(scope="session", autouse=True)
 93def evm_version(pytestconfig):
 94    # note: we configure the evm version that we emit code for,
 95    # but eth-tester is only configured with the latest mainnet
 96    # version.
 97    evm_version_str = pytestconfig.getoption("evm_version")
 98    evm.DEFAULT_EVM_VERSION = evm_version_str
 99    # this should get overridden by anchor_evm_version,
100    # but set it anyway
101    evm.active_evm_version = evm.EVM_VERSIONS[evm_version_str]
102
103
104@pytest.fixture
105def chdir_tmp_path(tmp_path):
106    # this is useful for when you want imports to have relpaths
107    with working_directory(tmp_path):
108        yield
109
110
111@pytest.fixture
112def keccak():
113    return Web3.keccak
114
115
116@pytest.fixture
117def make_file(tmp_path):
118    # writes file_contents to file_name, creating it in the
119    # tmp_path directory. returns final path.
120    def fn(file_name, file_contents):
121        path = tmp_path / file_name
122        path.parent.mkdir(parents=True, exist_ok=True)
123        with path.open("w") as f:
124            f.write(file_contents)
125
126        return path
127
128    return fn
129
130
131# this can either be used for its side effects (to prepare a call
132# to get_contract), or the result can be provided directly to
133# compile_code / CompilerData.
134@pytest.fixture
135def make_input_bundle(tmp_path, make_file):
136    def fn(sources_dict):
137        for file_name, file_contents in sources_dict.items():
138            make_file(file_name, file_contents)
139        return FilesystemInputBundle([tmp_path])
140
141    return fn
142
143
144# for tests which just need an input bundle, doesn't matter what it is
145@pytest.fixture
146def dummy_input_bundle():
147    return InputBundle([])
148
149
150# TODO: remove me, this is just string.encode("utf-8").ljust()
151# only used in test_logging.py.
152@pytest.fixture
153def bytes_helper():
154    def bytes_helper(str, length):
155        return bytes(str, "utf-8") + bytearray(length - len(str))
156
157    return bytes_helper
158
159
160def _none_addr(datatype, data):
161    if datatype == "address" and int(data, base=16) == 0:
162        return (datatype, None)
163    else:
164        return (datatype, data)
165
166
167CONCISE_NORMALIZERS = (_none_addr,)
168
169
170@pytest.fixture(scope="module")
171def tester():
172    # set absurdly high gas limit so that london basefee never adjusts
173    # (note: 2**63 - 1 is max that evm allows)
174    custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 10**10})
175    custom_genesis["base_fee_per_gas"] = 0
176    backend = PyEVMBackend(genesis_parameters=custom_genesis)
177    return EthereumTester(backend=backend)
178
179
180def zero_gas_price_strategy(web3, transaction_params=None):
181    return 0  # zero gas price makes testing simpler.
182
183
184@pytest.fixture(scope="module")
185def w3(tester):
186    w3 = Web3(EthereumTesterProvider(tester))
187    w3.eth.set_gas_price_strategy(zero_gas_price_strategy)
188    return w3
189
190
191def get_compiler_gas_estimate(code, func):
192    sigs = compiler.phases.CompilerData(code).function_signatures
193    if func:
194        return compiler.utils.build_gas_estimates(sigs)[func] + 22000
195    else:
196        return sum(compiler.utils.build_gas_estimates(sigs).values()) + 22000
197
198
199def check_gas_on_chain(w3, tester, code, func=None, res=None):
200    gas_estimate = get_compiler_gas_estimate(code, func)
201    gas_actual = tester.get_block_by_number("latest")["gas_used"]
202    # Computed upper bound on the gas consumption should
203    # be greater than or equal to the amount of gas used
204    if gas_estimate < gas_actual:
205        raise Exception(f"Gas upper bound fail: bound {gas_estimate} actual {gas_actual}")
206
207    print(f"Function name: {func} - Gas estimate {gas_estimate}, Actual: {gas_actual}")
208
209
210def gas_estimation_decorator(w3, tester, fn, source_code, func):
211    def decorator(*args, **kwargs):
212        @wraps(fn)
213        def decorated_function(*args, **kwargs):
214            result = fn(*args, **kwargs)
215            if "transact" in kwargs:
216                check_gas_on_chain(w3, tester, source_code, func, res=result)
217            return result
218
219        return decorated_function(*args, **kwargs)
220
221    return decorator
222
223
224def set_decorator_to_contract_function(w3, tester, contract, source_code, func):
225    func_definition = getattr(contract, func)
226    func_with_decorator = gas_estimation_decorator(w3, tester, func_definition, source_code, func)
227    setattr(contract, func, func_with_decorator)
228
229
230class VyperMethod:
231    ALLOWED_MODIFIERS = {"call", "estimateGas", "transact", "buildTransaction"}
232
233    def __init__(self, function, normalizers=None):
234        self._function = function
235        self._function._return_data_normalizers = normalizers
236
237    def __call__(self, *args, **kwargs):
238        return self.__prepared_function(*args, **kwargs)
239
240    def __prepared_function(self, *args, **kwargs):
241        if not kwargs:
242            modifier, modifier_dict = "call", {}
243            fn_abi = [
244                x
245                for x in self._function.contract_abi
246                if x.get("name") == self._function.function_identifier
247            ].pop()
248            # To make tests faster just supply some high gas value.
249            modifier_dict.update({"gas": fn_abi.get("gas", 0) + 500000})
250        elif len(kwargs) == 1:
251            modifier, modifier_dict = kwargs.popitem()
252            if modifier not in self.ALLOWED_MODIFIERS:
253                raise TypeError(f"The only allowed keyword arguments are: {self.ALLOWED_MODIFIERS}")
254        else:
255            raise TypeError(f"Use up to one keyword argument, one of: {self.ALLOWED_MODIFIERS}")
256        return getattr(self._function(*args), modifier)(modifier_dict)
257
258
259class VyperContract:
260    """
261    An alternative Contract Factory which invokes all methods as `call()`,
262    unless you add a keyword argument. The keyword argument assigns the prep method.
263    This call
264    > contract.withdraw(amount, transact={'from': eth.accounts[1], 'gas': 100000, ...})
265    is equivalent to this call in the classic contract:
266    > contract.functions.withdraw(amount).transact({'from': eth.accounts[1], 'gas': 100000, ...})
267    """
268
269    def __init__(self, classic_contract, method_class=VyperMethod):
270        classic_contract._return_data_normalizers += CONCISE_NORMALIZERS
271        self._classic_contract = classic_contract
272        self.address = self._classic_contract.address
273        protected_fn_names = [fn for fn in dir(self) if not fn.endswith("__")]
274
275        try:
276            fn_names = [fn["name"] for fn in self._classic_contract.functions._functions]
277        except web3.exceptions.NoABIFunctionsFound:
278            fn_names = []
279
280        for fn_name in fn_names:
281            # Override namespace collisions
282            if fn_name in protected_fn_names:
283                raise AttributeError(f"{fn_name} is protected!")
284            else:
285                _classic_method = getattr(self._classic_contract.functions, fn_name)
286                _concise_method = method_class(
287                    _classic_method, self._classic_contract._return_data_normalizers
288                )
289            setattr(self, fn_name, _concise_method)
290
291    @classmethod
292    def factory(cls, *args, **kwargs):
293        return compose(cls, Contract.factory(*args, **kwargs))
294
295
296@pytest.fixture
297def get_contract_from_ir(w3, optimize):
298    def ir_compiler(ir, *args, **kwargs):
299        ir = IRnode.from_list(ir)
300        if optimize != OptimizationLevel.NONE:
301            ir = optimizer.optimize(ir)
302
303        bytecode, _ = compile_ir.assembly_to_evm(
304            compile_ir.compile_to_assembly(ir, optimize=optimize)
305        )
306
307        abi = kwargs.get("abi") or []
308        c = w3.eth.contract(abi=abi, bytecode=bytecode)
309        deploy_transaction = c.constructor()
310        tx_hash = deploy_transaction.transact()
311        address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
312        contract = w3.eth.contract(
313            address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract
314        )
315        return contract
316
317    return ir_compiler
318
319
320def _get_contract(
321    w3,
322    source_code,
323    optimize,
324    output_formats,
325    *args,
326    override_opt_level=None,
327    input_bundle=None,
328    **kwargs,
329):
330    settings = Settings()
331    settings.optimize = override_opt_level or optimize
332    out = compiler.compile_code(
333        source_code,
334        # test that all output formats can get generated
335        output_formats=output_formats,
336        settings=settings,
337        input_bundle=input_bundle,
338        show_gas_estimates=True,  # Enable gas estimates for testing
339    )
340    parse_vyper_source(source_code)  # Test grammar.
341    json.dumps(out["metadata"])  # test metadata is json serializable
342    abi = out["abi"]
343    bytecode = out["bytecode"]
344    value = kwargs.pop("value_in_eth", 0) * 10**18  # Handle deploying with an eth value.
345    c = w3.eth.contract(abi=abi, bytecode=bytecode)
346    deploy_transaction = c.constructor(*args)
347    tx_info = {"from": w3.eth.accounts[0], "value": value, "gasPrice": 0}
348    tx_info.update(kwargs)
349    tx_hash = deploy_transaction.transact(tx_info)
350    address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
351    return w3.eth.contract(address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract)
352
353
354@pytest.fixture(scope="module")
355def get_contract(w3, optimize, output_formats):
356    def fn(source_code, *args, **kwargs):
357        return _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
358
359    return fn
360
361
362@pytest.fixture
363def get_contract_with_gas_estimation(tester, w3, optimize, output_formats):
364    def get_contract_with_gas_estimation(source_code, *args, **kwargs):
365        contract = _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
366        for abi_ in contract._classic_contract.functions.abi:
367            if abi_["type"] == "function":
368                set_decorator_to_contract_function(w3, tester, contract, source_code, abi_["name"])
369        return contract
370
371    return get_contract_with_gas_estimation
372
373
374@pytest.fixture
375def get_contract_with_gas_estimation_for_constants(w3, optimize, output_formats):
376    def get_contract_with_gas_estimation_for_constants(source_code, *args, **kwargs):
377        return _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
378
379    return get_contract_with_gas_estimation_for_constants
380
381
382@pytest.fixture(scope="module")
383def get_contract_module(optimize, output_formats):
384    """
385    This fixture is used for Hypothesis tests to ensure that
386    the same contract is called over multiple runs of the test.
387    """
388    custom_genesis = PyEVMBackend._generate_genesis_params(overrides={"gas_limit": 4500000})
389    custom_genesis["base_fee_per_gas"] = 0
390    backend = PyEVMBackend(genesis_parameters=custom_genesis)
391    tester = EthereumTester(backend=backend)
392    w3 = Web3(EthereumTesterProvider(tester))
393    w3.eth.set_gas_price_strategy(zero_gas_price_strategy)
394
395    def get_contract_module(source_code, *args, **kwargs):
396        return _get_contract(w3, source_code, optimize, output_formats, *args, **kwargs)
397
398    return get_contract_module
399
400
401def _deploy_blueprint_for(
402    w3, source_code, optimize, output_formats, initcode_prefix=ERC5202_PREFIX, **kwargs
403):
404    settings = Settings()
405    settings.optimize = optimize
406    out = compiler.compile_code(
407        source_code,
408        output_formats=output_formats,
409        settings=settings,
410        show_gas_estimates=True,  # Enable gas estimates for testing
411    )
412    parse_vyper_source(source_code)  # Test grammar.
413    abi = out["abi"]
414    bytecode = HexBytes(initcode_prefix) + HexBytes(out["bytecode"])
415    bytecode_len = len(bytecode)
416    bytecode_len_hex = hex(bytecode_len)[2:].rjust(4, "0")
417    # prepend a quick deploy preamble
418    deploy_preamble = HexBytes("61" + bytecode_len_hex + "3d81600a3d39f3")
419    deploy_bytecode = HexBytes(deploy_preamble) + bytecode
420
421    deployer_abi = []  # just a constructor
422    c = w3.eth.contract(abi=deployer_abi, bytecode=deploy_bytecode)
423    deploy_transaction = c.constructor()
424    tx_info = {"from": w3.eth.accounts[0], "value": 0, "gasPrice": 0}
425
426    tx_hash = deploy_transaction.transact(tx_info)
427    address = w3.eth.get_transaction_receipt(tx_hash)["contractAddress"]
428
429    # sanity check
430    assert w3.eth.get_code(address) == bytecode, (w3.eth.get_code(address), bytecode)
431
432    def factory(address):
433        return w3.eth.contract(
434            address, abi=abi, bytecode=bytecode, ContractFactoryClass=VyperContract
435        )
436
437    return w3.eth.contract(address, bytecode=deploy_bytecode), factory
438
439
440@pytest.fixture(scope="module")
441def deploy_blueprint_for(w3, optimize, output_formats):
442    def deploy_blueprint_for(source_code, *args, **kwargs):
443        return _deploy_blueprint_for(w3, source_code, optimize, output_formats, *args, **kwargs)
444
445    return deploy_blueprint_for
446
447
448# TODO: this should not be a fixture.
449# remove me and replace all uses with `with pytest.raises`.
450@pytest.fixture
451def assert_compile_failed():
452    def assert_compile_failed(function_to_test, exception=Exception):
453        with pytest.raises(exception):
454            function_to_test()
455
456    return assert_compile_failed
457
458
459@pytest.fixture
460def create2_address_of(keccak):
461    def _f(_addr, _salt, _initcode):
462        prefix = HexBytes("0xff")
463        addr = HexBytes(_addr)
464        salt = HexBytes(_salt)
465        initcode = HexBytes(_initcode)
466        return keccak(prefix + addr + salt + keccak(initcode))[12:]
467
468    return _f
469
470
471@pytest.fixture
472def side_effects_contract(get_contract):
473    def generate(ret_type):
474        """
475        Generates a Vyper contract with an external `foo()` function, which
476        returns the specified return value of the specified return type, for
477        testing side effects using the `assert_side_effects_invoked` fixture.
478        """
479        code = f"""
480counter: public(uint256)
481
482@external
483def foo(s: {ret_type}) -> {ret_type}:
484    self.counter += 1
485    return s
486    """
487        contract = get_contract(code)
488        return contract
489
490    return generate
491
492
493@pytest.fixture
494def assert_side_effects_invoked():
495    def assert_side_effects_invoked(side_effects_contract, side_effects_trigger, n=1):
496        start_value = side_effects_contract.counter()
497
498        side_effects_trigger()
499
500        end_value = side_effects_contract.counter()
501        assert end_value == start_value + n
502
503    return assert_side_effects_invoked
504
505
506@pytest.fixture
507def get_logs(w3):
508    def get_logs(tx_hash, c, event_name):
509        tx_receipt = w3.eth.get_transaction_receipt(tx_hash)
510        return c._classic_contract.events[event_name]().process_receipt(tx_receipt)
511
512    return get_logs
513
514
515@pytest.fixture(scope="module")
516def tx_failed(tester):
517    @contextmanager
518    def fn(exception=TransactionFailed, exc_text=None):
519        snapshot_id = tester.take_snapshot()
520        with pytest.raises(exception) as excinfo:
521            yield excinfo
522        tester.revert_to_snapshot(snapshot_id)
523        if exc_text:
524            # TODO test equality
525            assert exc_text in str(excinfo.value), (exc_text, excinfo.value)
526
527    return fn

The final two fixtures are optional and will be discussed later. The rest of this chapter assumes that you have this code set up in your conftest.py file.

Alternatively, you can import the fixtures to conftest.py or use pytest plugins.

Writing a Basic Test

Assume the following simple contract storage.vy. It has a single integer variable and a function to set that value.

storage.vy
 1#pragma version >0.3.10
 2
 3storedData: public(int128)
 4
 5@deploy
 6def __init__(_x: int128):
 7  self.storedData = _x
 8
 9@external
10def set(_x: int128):
11  self.storedData = _x

We create a test file test_storage.py where we write our tests in pytest style.

test_storage.py
 1import pytest
 2
 3INITIAL_VALUE = 4
 4
 5
 6@pytest.fixture
 7def storage_contract(w3, get_contract):
 8    with open("examples/storage/storage.vy") as f:
 9        contract_code = f.read()
10        # Pass constructor variables directly to the contract
11        contract = get_contract(contract_code, INITIAL_VALUE)
12    return contract
13
14
15def test_initial_state(storage_contract):
16    # Check if the constructor of the contract is set up properly
17    assert storage_contract.storedData() == INITIAL_VALUE
18
19
20def test_set(w3, storage_contract):
21    k0 = w3.eth.accounts[0]
22
23    # Let k0 try to set the value to 10
24    storage_contract.set(10, transact={"from": k0})
25    assert storage_contract.storedData() == 10  # Directly access storedData
26
27    # Let k0 try to set the value to -5
28    storage_contract.set(-5, transact={"from": k0})
29    assert storage_contract.storedData() == -5

First we create a fixture for the contract which will compile our contract and set up a Web3 contract object. We then use this fixture for our test functions to interact with the contract.

Note

To run the tests, call pytest or python -m pytest from your project directory.

Events and Failed Transactions

To test events and failed transactions we expand our simple storage contract to include an event and two conditions for a failed transaction: advanced_storage.vy

advanced_storage.vy
 1#pragma version >0.3.10
 2
 3event DataChange:
 4    setter: indexed(address)
 5    value: int128
 6
 7storedData: public(int128)
 8
 9@deploy
10def __init__(_x: int128):
11  self.storedData = _x
12
13@external
14def set(_x: int128):
15  assert _x >= 0, "No negative values"
16  assert self.storedData < 100, "Storage is locked when 100 or more is stored"
17  self.storedData = _x
18  log DataChange(msg.sender, _x)
19
20@external
21def reset():
22  self.storedData = 0

Next, we take a look at the two fixtures that will allow us to read the event logs and to check for failed transactions.

conftest.py
@pytest.fixture(scope="module")
def tx_failed(tester):
    @contextmanager
    def fn(exception=TransactionFailed, exc_text=None):
        snapshot_id = tester.take_snapshot()
        with pytest.raises(exception) as excinfo:
            yield excinfo
        tester.revert_to_snapshot(snapshot_id)
        if exc_text:
            # TODO test equality
            assert exc_text in str(excinfo.value), (exc_text, excinfo.value)

    return fn

The fixture to assert failed transactions defaults to check for a TransactionFailed exception, but can be used to check for different exceptions too, as shown below. Also note that the chain gets reverted to the state before the failed transaction.

conftest.py
@pytest.fixture
def get_logs(w3):
    def get_logs(tx_hash, c, event_name):
        tx_receipt = w3.eth.get_transaction_receipt(tx_hash)
        return c._classic_contract.events[event_name]().process_receipt(tx_receipt)

    return get_logs

This fixture will return a tuple with all the logs for a certain event and transaction. The length of the tuple equals the number of events (of the specified type) logged and should be checked first.

Finally, we create a new file test_advanced_storage.py where we use the new fixtures to test failed transactions and events.

test_advanced_storage.py
 1import pytest
 2from web3.exceptions import ValidationError
 3
 4INITIAL_VALUE = 4
 5
 6
 7@pytest.fixture
 8def adv_storage_contract(w3, get_contract):
 9    with open("examples/storage/advanced_storage.vy") as f:
10        contract_code = f.read()
11        # Pass constructor variables directly to the contract
12        contract = get_contract(contract_code, INITIAL_VALUE)
13    return contract
14
15
16def test_initial_state(adv_storage_contract):
17    # Check if the constructor of the contract is set up properly
18    assert adv_storage_contract.storedData() == INITIAL_VALUE
19
20
21def test_failed_transactions(w3, adv_storage_contract, tx_failed):
22    k1 = w3.eth.accounts[1]
23
24    # Try to set the storage to a negative amount
25    with tx_failed():
26        adv_storage_contract.set(-10, transact={"from": k1})
27
28    # Lock the contract by storing more than 100. Then try to change the value
29    adv_storage_contract.set(150, transact={"from": k1})
30    with tx_failed():
31        adv_storage_contract.set(10, transact={"from": k1})
32
33    # Reset the contract and try to change the value
34    adv_storage_contract.reset(transact={"from": k1})
35    adv_storage_contract.set(10, transact={"from": k1})
36    assert adv_storage_contract.storedData() == 10
37
38    # Assert a different exception (ValidationError for non-matching argument type)
39    with tx_failed(ValidationError):
40        adv_storage_contract.set("foo", transact={"from": k1})
41
42    # Assert a different exception that contains specific text
43    with tx_failed(ValidationError, "invocation failed due to improper number of arguments"):
44        adv_storage_contract.set(1, 2, transact={"from": k1})
45
46
47def test_events(w3, adv_storage_contract, get_logs):
48    k1, k2 = w3.eth.accounts[:2]
49
50    tx1 = adv_storage_contract.set(10, transact={"from": k1})
51    tx2 = adv_storage_contract.set(20, transact={"from": k2})
52    tx3 = adv_storage_contract.reset(transact={"from": k1})
53
54    # Save DataChange logs from all three transactions
55    logs1 = get_logs(tx1, adv_storage_contract, "DataChange")
56    logs2 = get_logs(tx2, adv_storage_contract, "DataChange")
57    logs3 = get_logs(tx3, adv_storage_contract, "DataChange")
58
59    # Check log contents
60    assert len(logs1) == 1
61    assert logs1[0].args.value == 10
62
63    assert len(logs2) == 1
64    assert logs2[0].args.setter == k2
65
66    assert not logs3  # tx3 does not generate a log