From 6eddb31f1874a3f863f41c7c79c89182945252cb Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 13:45:53 +0300 Subject: [PATCH 01/72] allow skipping tests only during coverage --- brownie/cli/test.py | 2 +- docs/tests.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 32eb4d133..2724eefb3 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -54,7 +54,7 @@ def _run_test(module, fn_name, count, total): fn.__code__.co_varnames[:len(fn.__defaults__)], fn.__defaults__ )) - if 'skip' in args and args['skip']: + if 'skip' in args and (args['skip']==True or (args['skip']=="coverage" and ARGV['coverage'])): sys.stdout.write( "\r {0[pending]}\u229d{0[dull]} {1} - ".format(color, count) + "{1} ({0[pending]}skipped{0[dull]}){0}\n".format(color, desc) diff --git a/docs/tests.rst b/docs/tests.rst index 971d9d200..b929abe31 100644 --- a/docs/tests.rst +++ b/docs/tests.rst @@ -49,7 +49,7 @@ You can optionally include a docstring in each test method to give more verbosit The following keyword arguments can be used to affect how a test runs: -* ``skip``: If set to ``True``, this test will not be run. +* ``skip``: If set to ``True``, this test will not be run. If set to ``coverage``, the test will only be skipped during coverage evaluation. * ``pending``: If set to ``True``, this test is expected to fail. If the test passes it will raise an ``ExpectedFailing`` exception. * ``always_transact``: If set to ``False``, calls to non state-changing methods will still execute as calls when running test coverage analysis. See :ref:`coverage` for more information. From d5014078d59a6bbc273ad4c75a7b6addffb3725e Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 13:46:38 +0300 Subject: [PATCH 02/72] bugfix - relaunch RPC --- brownie/cli/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 2724eefb3..82ce1b15e 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -140,7 +140,7 @@ def run_test(filename, network, idx): if traceback_info and traceback_info[-1][2] == ReadTimeout: print(" {0[error]}WARNING{0}: RPC crashed, terminating test".format(color)) network.rpc.kill(False) - network.rpc.launch() + network.rpc.launch(CONFIG['active_network']['test-rpc']) break test_history.update(TxHistory().copy()) return test_history, traceback_info From 56f8010117dcb6165fe83ea65d1ba8308843e1df Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 19:09:28 +0300 Subject: [PATCH 03/72] use relative paths in build json, closes #111 --- CHANGELOG | 5 +++++ brownie/cli/test.py | 5 ++--- brownie/project/compiler.py | 4 +++- brownie/project/sources.py | 7 +++++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3d403025a..075cb8c52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +1.0.0b5 +------- + + - Use relative paths in build json files + 1.0.0b4 ------- diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 82ce1b15e..558481586 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -220,8 +220,7 @@ def main(): "Evaluating test coverage ({:.4f}s)\n".format(time.time()-stime) ) sys.stdout.flush() - build_folder = Path(CONFIG['folders']['project']).joinpath('build/contracts') - build_files = set(build_folder.joinpath(i+'.json') for i in coverage_eval) + build_files = set(Path('build/contracts/{}.json'.format(i)) for i in coverage_eval) coverage_eval = { 'coverage': coverage_eval, 'sha1': dict((str(i), Build()[i.stem]['bytecodeSha1']) for i in build_files) @@ -229,7 +228,7 @@ def main(): if args['']: continue - test_path = Path(CONFIG['folders']['project']).joinpath(filename+".py") + test_path = Path(filename+".py") coverage_eval['sha1'][str(test_path)] = get_ast_hash(test_path) json.dump( diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 66eeb009d..e47249608 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -50,7 +50,9 @@ def compile_contracts(contract_paths): CONFIG['solc']['optimize'] else "Disabled" )) - print("\n".join(" - {}...".format(Path(i).name) for i in contract_paths)) + base = Path(CONFIG['folders']['project']) + contract_paths = [Path(i).resolve().relative_to(base) for i in contract_paths] + print("\n".join(" - {}...".format(i.name) for i in contract_paths)) input_json = STANDARD_JSON.copy() input_json['sources'] = dict(( str(i), diff --git a/brownie/project/sources.py b/brownie/project/sources.py index f3c26684a..7804c36f3 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -18,8 +18,11 @@ def __init__(self): self._string_iter = 1 def _load(self): - self. _path = Path(CONFIG['folders']['project']).joinpath('contracts') - for path in [i for i in self._path.glob('**/*.sol') if "/_" not in str(i)]: + base_path = Path(CONFIG['folders']['project']) + self. _path = base_path.joinpath('contracts') + for path in [i.relative_to(base_path) for i in self._path.glob('**/*.sol')]: + if "/_" in str(path): + continue self._source[str(path)] = path.open().read() self._get_contract_data(path) for name, inherited in [(k, v['inherited'].copy()) for k,v in self._data.items()]: From f05ad75cfc68897a2b6aa3da3e24c7ffbafdc199 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 21:29:11 +0300 Subject: [PATCH 04/72] add _revert_lock internal --- brownie/network/history.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brownie/network/history.py b/brownie/network/history.py index b2e625a2a..f4611521b 100644 --- a/brownie/network/history.py +++ b/brownie/network/history.py @@ -17,6 +17,7 @@ class TxHistory(metaclass=_Singleton): def __init__(self): self._list = [] + self._revert_lock = False def __bool__(self): return bool(self._list) @@ -37,9 +38,10 @@ def _reset(self): self._list.clear() def _revert(self): + if self._revert_lock: + return height = web3.eth.blockNumber - for tx in [i for i in self._list if i.block_number > height]: - self._list.remove(tx) + self._list = [i for i in self._list if i.block_number <= height] def _console_repr(self): return str(self._list) From 67f865e0a1ac1f1b26a6624c4b5b2b35676836a8 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 21:32:56 +0300 Subject: [PATCH 05/72] use FalseyDict for fn args, streamline calls to TxHistory --- brownie/cli/test.py | 49 +++++++++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 558481586..eb16585a1 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -17,6 +17,7 @@ from brownie.network.history import TxHistory import brownie.network.transaction as transaction from brownie.project.build import Build, get_ast_hash +from brownie.types import FalseyDict from brownie._config import ARGV, CONFIG COVERAGE_COLORS = [ @@ -44,24 +45,27 @@ subfolders. Files and folders beginning with an underscore will be skipped.""" +def _get_args(fn): + if not fn.__defaults__: + return FalseyDict() + return FalseyDict(zip( + fn.__code__.co_varnames[:len(fn.__defaults__)], + fn.__defaults__ + )) + + def _run_test(module, fn_name, count, total): fn = getattr(module, fn_name) desc = fn.__doc__ or fn_name sys.stdout.write(" {0} - {1} ({0}/{2})...".format(count, desc, total)) sys.stdout.flush() - if fn.__defaults__: - args = dict(zip( - fn.__code__.co_varnames[:len(fn.__defaults__)], - fn.__defaults__ - )) - if 'skip' in args and (args['skip']==True or (args['skip']=="coverage" and ARGV['coverage'])): - sys.stdout.write( - "\r {0[pending]}\u229d{0[dull]} {1} - ".format(color, count) + - "{1} ({0[pending]}skipped{0[dull]}){0}\n".format(color, desc) - ) - return [] - else: - args = {} + args = _get_args(fn) + if args['skip'] == True or (args['skip']=="coverage" and ARGV['coverage']): + sys.stdout.write( + "\r {0[pending]}\u229d{0[dull]} {1} - ".format(color, count) + + "{1} ({0[pending]}skipped{0[dull]}){0}\n".format(color, desc) + ) + return [] try: stime = time.time() if ARGV['coverage'] and 'always_transact' in args: @@ -69,7 +73,7 @@ def _run_test(module, fn_name, count, total): fn() if ARGV['coverage']: ARGV['always_transact'] = True - if 'pending' in args and args['pending']: + if args['pending']: raise ExpectedFailing("Test was expected to fail") sys.stdout.write("\r {0[success]}\u2713{0} {1} - {2} ({3:.4f}s) \n".format( color, count, desc, time.time()-stime @@ -77,7 +81,7 @@ def _run_test(module, fn_name, count, total): sys.stdout.flush() return [] except Exception as e: - if type(e) != ExpectedFailing and 'pending' in args and args['pending']: + if type(e) != ExpectedFailing and args['pending']: c = [color('success'), color('dull'), color()] else: c = [color('error'), color('dull'), color()] @@ -89,7 +93,7 @@ def _run_test(module, fn_name, count, total): count )) sys.stdout.flush() - if type(e) != ExpectedFailing and 'pending' in args and args['pending']: + if type(e) != ExpectedFailing and args['pending']: return [] filename = str(Path(module.__file__).relative_to(CONFIG['folders']['project'])) fn_name = filename[:-2]+fn_name @@ -117,7 +121,6 @@ def run_test(filename, network, idx): ) ) traceback_info = [] - test_history = set() if not test_names: print("\n{0[error]}WARNING{0}: No test functions in {0[module]}{1}.py{0}".format(color, filename)) return [], [] @@ -132,7 +135,7 @@ def run_test(filename, network, idx): test_names.remove('setup') traceback_info += _run_test(module, 'setup', 0, len(test_names)) if traceback_info: - return TxHistory().copy(), traceback_info + return traceback_info network.rpc.snapshot() for c, t in enumerate(test_names[idx], start=idx.start + 1): network.rpc.revert() @@ -142,8 +145,7 @@ def run_test(filename, network, idx): network.rpc.kill(False) network.rpc.launch(CONFIG['active_network']['test-rpc']) break - test_history.update(TxHistory().copy()) - return test_history, traceback_info + return traceback_info def get_test_files(path): @@ -168,6 +170,8 @@ def main(): ARGV._update_from_args(args) if ARGV['coverage']: ARGV['always_transact'] = True + history = TxHistory() + history._revert_lock = True traceback_info = [] test_files = get_test_files(args['']) @@ -203,7 +207,8 @@ def main(): if not p.exists(): p.mkdir() - test_history, tb = run_test(filename, network, idx) + + tb = run_test(filename, network, idx) if tb: traceback_info += tb if coverage_json.exists(): @@ -214,7 +219,7 @@ def main(): stime = time.time() sys.stdout.write(" - Evaluating test coverage...") sys.stdout.flush() - coverage_eval = analyze_coverage(test_history) + coverage_eval = analyze_coverage(history.copy()) sys.stdout.write( "\r {0[success]}\u2713{0} - ".format(color) + "Evaluating test coverage ({:.4f}s)\n".format(time.time()-stime) From 16498125313777f0f0b91fd6d2b78b4e205e2aa1 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 21:57:59 +0300 Subject: [PATCH 06/72] modify import sequence around Rpc._revert and Rpc._reset --- brownie/network/account.py | 2 ++ brownie/network/history.py | 3 +++ brownie/network/rpc.py | 34 +++++++++++++--------------------- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/brownie/network/account.py b/brownie/network/account.py index 3130cb12f..8023d502c 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -12,6 +12,7 @@ from brownie.cli.utils import color from brownie.exceptions import VirtualMachineError from brownie.network.transaction import TransactionReceipt +from .rpc import Rpc from .web3 import Web3 from brownie.types.convert import to_address, wei from brownie.types.types import _Singleton @@ -28,6 +29,7 @@ def __init__(self): self._accounts = [] # prevent private keys from being stored in read history self.add.__dict__['_private'] = True + Rpc()._objects.append(self) def _reset(self): self._accounts.clear() diff --git a/brownie/network/history.py b/brownie/network/history.py index f4611521b..dfccc4b07 100644 --- a/brownie/network/history.py +++ b/brownie/network/history.py @@ -2,6 +2,7 @@ from collections import OrderedDict +from .rpc import Rpc from brownie.types.types import _Singleton from brownie.types.convert import to_address from .web3 import Web3 @@ -18,6 +19,7 @@ class TxHistory(metaclass=_Singleton): def __init__(self): self._list = [] self._revert_lock = False + Rpc()._objects.append(self) def __bool__(self): return bool(self._list) @@ -73,6 +75,7 @@ class _ContractHistory(metaclass=_Singleton): def __init__(self): self._dict = {} + Rpc()._objects.append(self) def _reset(self): self._dict.clear() diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index dc7fdf4b4..d15b8af19 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -4,15 +4,13 @@ import psutil from subprocess import DEVNULL, PIPE import sys -from threading import Thread import time from .web3 import Web3 -from .account import Accounts -from .history import TxHistory, _ContractHistory + from brownie.types.types import _Singleton from brownie.exceptions import RPCProcessError, RPCConnectionError -from brownie._config import CONFIG + web3 = Web3() @@ -30,6 +28,7 @@ def __init__(self): self._time_offset = 0 self._snapshot_id = False self._reset_id = False + self._objects = [] atexit.register(self._at_exit) def _at_exit(self): @@ -74,13 +73,13 @@ def launch(self, cmd): raise RPCProcessError(cmd, self._rpc, uri) # check that web3 can connect if not web3.providers: - _reset() + self._reset() return for i in range(50): time.sleep(0.05) if web3.isConnected(): self._reset_id = self._snap() - _reset() + self._reset() return rpc = self._rpc self.kill(False) @@ -104,7 +103,7 @@ def attach(self, laddr): self._rpc = psutil.Process(proc.pid) if web3.providers: self._reset_id = self._snap() - _reset() + self._reset() def kill(self, exc=True): '''Terminates the RPC process and all children with SIGKILL. @@ -125,7 +124,7 @@ def kill(self, exc=True): self._snapshot_id = False self._reset_id = False self._rpc = None - _reset() + self._reset() def _request(self, *args): if not self.is_active(): @@ -144,9 +143,14 @@ def _revert(self, id_): self._request("evm_revert", [id_]) id_ = self._snap() self.sleep(0) - _revert() + for i in self._objects: + i._revert() return id_ + def _reset(self): + for i in self._objects: + i._reset() + def is_active(self): '''Returns True if Rpc client is currently active.''' if not self._rpc: @@ -204,15 +208,3 @@ def reset(self): self._snapshot_id = None self._reset_id = self._revert(self._reset_id) return "Block height reset to 0" - - -def _reset(): - TxHistory()._reset() - _ContractHistory()._reset() - Accounts()._reset() - - -def _revert(): - TxHistory()._revert() - _ContractHistory()._revert() - Accounts()._revert() From 0b019bd09f95132b22352383c9fd41aeaa1f0702 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 21:59:49 +0300 Subject: [PATCH 07/72] revert after call-as-tx - closes #112 --- brownie/network/contract.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 110272d5c..013526aff 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -8,6 +8,7 @@ from brownie.cli.utils import color from .event import get_topics from .history import _ContractHistory +from .rpc import Rpc from .web3 import Web3 from brownie.types import KwargTuple from brownie.types.convert import format_input, format_output, to_address @@ -15,6 +16,7 @@ from brownie._config import ARGV, CONFIG _contracts = _ContractHistory() +rpc = Rpc() web3 = Web3() @@ -367,7 +369,9 @@ def __call__(self, *args): Returns: Contract method return value(s).''' if ARGV['always_transact']: + id_ = rpc._snap() tx = self.transact(*args) + rpc._revert(id_) return tx.return_value return self.call(*args) From 6a48d94a290497d3a2ec9c4d2b7dfc5f5012b2f3 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 8 May 2019 22:04:57 +0300 Subject: [PATCH 08/72] update docs --- CHANGELOG | 1 + docs/coverage.rst | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 075cb8c52..f1d8cddbc 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,7 @@ ------- - Use relative paths in build json files + - Revert calls-as-transactions when evaluating coverage 1.0.0b4 ------- diff --git a/docs/coverage.rst b/docs/coverage.rst index 284b4c46e..a36966fb1 100644 --- a/docs/coverage.rst +++ b/docs/coverage.rst @@ -8,7 +8,7 @@ Checking Test Coverage Test coverage is estimated by generating a map of opcodes associated with each function and line of the smart contract source code, and then analyzing the stack trace of each transaction to see which opcodes were executed. -When analyzing coverage, contract calls are executed as transactions. This allows analysis for non state-changing methods, however it also means that calls will consume gas, increase the block height and increase the nonce of an address. You can prevent this behaviour by adding ``always_transact=False`` as a keyword argument for a test. +When analyzing coverage, contract calls are executed as transactions to allow analysis for non state-changing methods. After each call, the state of the local chain is reverted to immediately before the call. For tests that involve many calls this can result in significantly slower execution time. You can prevent this behaviour by adding ``always_transact=False`` as a keyword argument for a test. To check your unit test coverage, type: From c6af9816f6ae2aed8b9d8e192316bea858c56620 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 02:01:41 +0300 Subject: [PATCH 09/72] bugfix - is_active() --- brownie/network/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index d15b8af19..55fc68d94 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -161,7 +161,7 @@ def is_active(self): def is_child(self): '''Returns True if the Rpc client is active and was launched by Brownie.''' - if not is_active(): + if not self.is_active(): return False return self._rpc.parent() == psutil.Process() From 57b601b72ebb333a6ad679e8aa6e2d32e83135c7 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 02:02:07 +0300 Subject: [PATCH 10/72] optimizations --- brownie/network/transaction.py | 48 +++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 046d9728d..02afca6d3 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -288,17 +288,17 @@ def _evaluate_trace(self): self.trace = trace = self._trace if not trace: return - c = self.contract_address or self.receiver - last = {0: { - 'address': c.address, - 'contract': c._name, + contract = self.contract_address or self.receiver + last_map = {0: { + 'address': contract.address, + 'contract': contract, 'fn': [self.fn_name.split('.')[-1]], }} - pc = c._build['pcMap'][0] + pc = contract._build['pcMap'][0] trace[0].update({ - 'address': last[0]['address'], - 'contractName': last[0]['contract'], - 'fn': last[0]['fn'][-1], + 'address': last_map[0]['address'], + 'contractName': last_map[0]['contract']._name, + 'fn': last_map[0]['fn'][-1], 'jumpDepth': 1, 'source': { 'filename': pc['contract'], @@ -310,23 +310,24 @@ def _evaluate_trace(self): # if depth has increased, tx has called into a different contract if trace[i]['depth'] > trace[i-1]['depth']: address = web3.toChecksumAddress(trace[i-1]['stack'][-2][-40:]) - c = _contracts.find(address) + contract = _contracts.find(address) stack_idx = -4 if trace[i-1]['op'] in ('CALL', 'CALLCODE') else -3 memory_idx = int(trace[i-1]['stack'][stack_idx], 16) * 2 sig = "0x" + "".join(trace[i-1]['memory'])[memory_idx:memory_idx+8] - last[trace[i]['depth']] = { + last_map[trace[i]['depth']] = { 'address': address, - 'contract': c._name, - 'fn': [next((k for k, v in c.signatures.items() if v == sig), "")], + 'contract': contract, + 'fn': [contract.get_method(sig) or ""], } + last = last_map[trace[i]['depth']] + contract = last['contract'] trace[i].update({ - 'address': last[trace[i]['depth']]['address'], - 'contractName': last[trace[i]['depth']]['contract'], - 'fn': last[trace[i]['depth']]['fn'][-1], - 'jumpDepth': len(set(last[trace[i]['depth']]['fn'])) + 'address': last['address'], + 'contractName': contract._name, + 'fn': last['fn'][-1], + 'jumpDepth': len(set(last['fn'])) }) - c = _contracts.find(trace[i]['address']) - pc = c._build['pcMap'][trace[i]['pc']] + pc = contract._build['pcMap'][trace[i]['pc']] trace[i]['source'] = { 'filename': pc['contract'], 'start': pc['start'], @@ -334,15 +335,15 @@ def _evaluate_trace(self): } # jump 'i' is moving into an internal function if pc['jump'] == 'i': - source = c._build['source'][pc['start']:pc['stop']] + source = contract._build['source'][pc['start']:pc['stop']] if source[:7] not in ("library", "contrac") and "(" in source: fn = source[:source.index('(')].split('.')[-1] else: - fn = last[trace[i]['depth']]['fn'][-1] - last[trace[i]['depth']]['fn'].append(fn) + fn = last['fn'][-1] + last['fn'].append(fn) # jump 'o' is coming out of an internal function - elif pc['jump'] == "o" and len(last[trace[i]['depth']]['fn']) > 1: - last[trace[i]['depth']]['fn'].pop() + elif pc['jump'] == "o" and len(['fn']) > 1: + del last['fn'][-1] def call_trace(self): '''Displays the sequence of contracts and functions called while @@ -375,7 +376,6 @@ def error(self, pad=3): idx -= 1 continue span = (self.trace[idx]['source']['start'], self.trace[idx]['source']['stop']) - c = _contracts.find(self.trace[idx]['address']) if sources.get_type(self.trace[idx]['contractName']) in ("contract", "library"): idx -= 1 continue From 4d72d0bc90cc1111dac44b21e1766c8c38e56968 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 02:41:35 +0300 Subject: [PATCH 11/72] allow gas_price == 0 --- brownie/network/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/network/account.py b/brownie/network/account.py index 8023d502c..f7b7675f1 100644 --- a/brownie/network/account.py +++ b/brownie/network/account.py @@ -256,7 +256,7 @@ def transfer(self, to, amount, gas_limit=None, gas_price=None, data=""): txid = self._transact({ 'from': self.address, 'nonce': self.nonce, - 'gasPrice': wei(gas_price) or self._gas_price(), + 'gasPrice': wei(gas_price) if gas_price is not None else self._gas_price(), 'gas': wei(gas_limit) or self._gas_limit(to, amount, data), 'to': str(to), 'value': wei(amount), From c3c0e14f194f4059501b57edc21ced9c43f222db Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 02:49:56 +0300 Subject: [PATCH 12/72] add TransactionReceipt.modified_state --- brownie/network/transaction.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 02afca6d3..a20beed6f 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -182,7 +182,7 @@ def __hash__(self): return hash(self.txid) def __getattr__(self, attr): - if attr not in ('events', 'return_value', 'revert_msg', 'trace', '_trace'): + if attr not in ('events', 'modified_state', 'return_value', 'revert_msg', 'trace', '_trace'): raise AttributeError( "'TransactionReceipt' object has no attribute '{}'".format(attr) ) @@ -230,6 +230,7 @@ def _get_trace(self): self.revert_msg = None self._trace = [] if (self.input == "0x" and self.gas_used == 21000) or self.contract_address: + self.modified_state = False self.trace = [] return trace = web3.providers[0].make_request( @@ -237,19 +238,21 @@ def _get_trace(self): [self.txid, {}] ) if 'error' in trace: + self.modified_state = None raise ValueError(trace['error']['message']) self._trace = trace = trace['result']['structLogs'] if self.status: # get return value + self.modified_state = bool(next((i for i in trace if i['op'] == "SSTORE"), False)) log = trace[-1] if log['op'] != "RETURN": return - c = self.contract_address or self.receiver - if type(c) is str: + contract = self.contract_address or self.receiver + if type(contract) is str: return abi = [ i['type'] for i in - getattr(c, self.fn_name.split('.')[-1]).abi['outputs'] + getattr(contract, self.fn_name.split('.')[-1]).abi['outputs'] ] offset = int(log['stack'][-1], 16) * 2 length = int(log['stack'][-2], 16) * 2 @@ -262,10 +265,11 @@ def _get_trace(self): else: self.return_value = KwargTuple( self.return_value, - getattr(c, self.fn_name.split('.')[-1]).abi + getattr(contract, self.fn_name.split('.')[-1]).abi ) else: # get revert message + self.modified_state = False self.revert_msg = "" if trace[-1]['op'] == "REVERT": offset = int(trace[-1]['stack'][-1], 16) * 2 From d3c5d7d6af3233e0529382c0363c7850f15f98dc Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 02:52:06 +0300 Subject: [PATCH 13/72] only revert when state was modified, more efficient snapshotting --- brownie/network/contract.py | 8 +++++--- brownie/network/rpc.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 013526aff..010a969bf 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -302,6 +302,7 @@ def transact(self, *args): "Contract has no owner, you must supply a tx dict" " with a 'from' field as the last argument." ) + rpc._internal_clear() return tx['from'].transfer( self._address, tx['value'], @@ -369,9 +370,10 @@ def __call__(self, *args): Returns: Contract method return value(s).''' if ARGV['always_transact']: - id_ = rpc._snap() - tx = self.transact(*args) - rpc._revert(id_) + rpc._internal_snap() + tx = self.transact(*args, {'gas_price': 0}) + if tx.modified_state: + rpc._internal_revert() return tx.return_value return self.call(*args) diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index 55fc68d94..0f029aa65 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -27,6 +27,7 @@ def __init__(self): self._rpc = None self._time_offset = 0 self._snapshot_id = False + self._internal_id = False self._reset_id = False self._objects = [] atexit.register(self._at_exit) @@ -208,3 +209,17 @@ def reset(self): self._snapshot_id = None self._reset_id = self._revert(self._reset_id) return "Block height reset to 0" + + def _internal_snap(self): + if not self._internal_id: + self._internal_id = self._snap() + + def _internal_clear(self): + self._internal_id = None + + def _internal_revert(self): + self._request("evm_revert", [self._internal_id]) + self._internal_id = None + self.sleep(0) + for i in self._objects: + i._revert() From 56411369b24d5695a46f456c64eb8de6afa22ea9 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 03:03:42 +0300 Subject: [PATCH 14/72] update docs --- docs/api-network.rst | 11 +++++++++++ docs/coverage.rst | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/api-network.rst b/docs/api-network.rst index d67effa27..8c665ca58 100644 --- a/docs/api-network.rst +++ b/docs/api-network.rst @@ -1171,6 +1171,17 @@ TransactionReceipt Attributes >>> tx.logs [AttributeDict({'logIndex': 0, 'transactionIndex': 0, 'transactionHash': HexBytes('0xa8afb59a850adff32548c65041ec253eb64e1154042b2e01e2cd8cddb02eb94f'), 'blockHash': HexBytes('0x0b93b4cf230c9ef92b990de9cd62611447d83d396f1b13204d26d28bd949543a'), 'blockNumber': 6, 'address': '0x79447c97b6543F6eFBC91613C655977806CB18b0', 'data': '0x0000000000000000000000006b5132740b834674c3277aafa2c27898cbe740f600000000000000000000000031d504908351d2d87f3d6111f491f0b52757b592000000000000000000000000000000000000000000000000000000000000000a', 'topics': [HexBytes('0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef')], 'type': 'mined'})] +.. py:attribute:: TransactionReceipt.modified_state + + Boolean indicating if this transaction resuled in any state changes on the blockchain. + + .. code-block:: python + + >>> tx + + >>> tx.modified_state + True + .. py:attribute:: TransactionReceipt.nonce The nonce of the transaction. diff --git a/docs/coverage.rst b/docs/coverage.rst index a36966fb1..bbdcbd660 100644 --- a/docs/coverage.rst +++ b/docs/coverage.rst @@ -8,7 +8,7 @@ Checking Test Coverage Test coverage is estimated by generating a map of opcodes associated with each function and line of the smart contract source code, and then analyzing the stack trace of each transaction to see which opcodes were executed. -When analyzing coverage, contract calls are executed as transactions to allow analysis for non state-changing methods. After each call, the state of the local chain is reverted to immediately before the call. For tests that involve many calls this can result in significantly slower execution time. You can prevent this behaviour by adding ``always_transact=False`` as a keyword argument for a test. +During analysis, all contract calls are executed as transactions. This gives a more accurate coverage picture by allowing analysis of methods that are typically non-state changing. Whenever one of these calls-as-transactions results in a state change, the blockchain will be reverted to ensure that the outcome of the test is not effected. For tests that involve many calls this can result in significantly slower execution time. You can prevent this behaviour by adding ``always_transact=False`` as a keyword argument for a test. To check your unit test coverage, type: From b29017eadcc1cf0583c2edc79f0346f4418cbe4c Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 04:52:19 +0300 Subject: [PATCH 15/72] source offsets --- brownie/project/sources.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 7804c36f3..9931c1510 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -8,10 +8,14 @@ from brownie._config import CONFIG +COMMENTS_REGEX = "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + class Sources(metaclass=_Singleton): def __init__(self): self._source = {} + self._uncommented = {} + self._comment_offsets = {} self._path = None self._data = {} self._inheritance_map = {} @@ -23,15 +27,28 @@ def _load(self): for path in [i.relative_to(base_path) for i in self._path.glob('**/*.sol')]: if "/_" in str(path): continue - self._source[str(path)] = path.open().read() + source = path.open().read() + self._source[str(path)] = source + self._remove_comments(path) self._get_contract_data(path) for name, inherited in [(k, v['inherited'].copy()) for k,v in self._data.items()]: self._data[name]['inherited'] = self._recursive_inheritance(inherited) + def _remove_comments(self, path): + source = self._source[str(path)] + offsets = [(0, 0)] + for match in re.finditer(COMMENTS_REGEX, source): + offsets.append(( + match.start() - offsets[-1][1], + match.end() - match.start() + offsets[-1][1] + )) + self._uncommented[str(path)] = re.sub(COMMENTS_REGEX, "", source) + self._comment_offsets[str(path)] = offsets[::-1] + def _get_contract_data(self, path): contracts = re.findall( "((?:contract|library|interface)[\s\S]*?})\s*(?=contract|library|interface|$)", - self.remove_comments(path) + self._uncommented[str(path)] ) for source in contracts: type_, name, inherited = re.findall( @@ -39,11 +56,14 @@ def _get_contract_data(self, path): source )[0] inherited = set(i.strip() for i in inherited.split(', ') if i) + self._data[name] = { 'sourcePath': path, 'type': type_, 'inherited': inherited.union(re.findall("(?:;|{)\s*using *(\S*)(?= for)", source)), - 'sha1': sha1(source.encode()).hexdigest() + 'sha1': sha1(source.encode()).hexdigest(), + 'start': self.original_offset(path, self._uncommented[str(path)].index(source)), + 'stop': self.original_offset(path, self._uncommented[str(path)].index(source)+len(source)) } def _recursive_inheritance(self, inherited): @@ -51,13 +71,6 @@ def _recursive_inheritance(self, inherited): for name in inherited: final |= self._recursive_inheritance(self._data[name]['inherited']) return final - - def remove_comments(self, path): - return re.sub( - "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))", - "", - self._source[str(path)] - ) def get_hash(self, contract_name): return self._data[contract_name]['sha1'] @@ -81,4 +94,7 @@ def add_source(self, source): self._source[path] = source self._get_contract_data(path) self._string_iter += 1 - return path \ No newline at end of file + return path + + def original_offset(self, path, offset): + return offset + next(i[1] for i in self._comment_offsets[str(path)] if i[0]<=offset) \ No newline at end of file From e13a95e29b8db8492520c108b7666a2f9e410ada Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 16:59:54 +0300 Subject: [PATCH 16/72] function offsets --- brownie/project/sources.py | 73 +++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 9931c1510..15b591d0f 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -1,26 +1,25 @@ #!/usr/bin/python3 +from hashlib import sha1 from pathlib import Path import re -from hashlib import sha1 from brownie.types.types import _Singleton from brownie._config import CONFIG -COMMENTS_REGEX = "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" - class Sources(metaclass=_Singleton): def __init__(self): self._source = {} - self._uncommented = {} + self._uncommented_source = {} self._comment_offsets = {} + self._fn_map = {} self._path = None self._data = {} self._inheritance_map = {} self._string_iter = 1 - + def _load(self): base_path = Path(CONFIG['folders']['project']) self. _path = base_path.joinpath('contracts') @@ -31,24 +30,25 @@ def _load(self): self._source[str(path)] = source self._remove_comments(path) self._get_contract_data(path) - for name, inherited in [(k, v['inherited'].copy()) for k,v in self._data.items()]: + for name, inherited in [(k, v['inherited'].copy()) for k, v in self._data.items()]: self._data[name]['inherited'] = self._recursive_inheritance(inherited) def _remove_comments(self, path): source = self._source[str(path)] offsets = [(0, 0)] - for match in re.finditer(COMMENTS_REGEX, source): + pattern = "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + for match in re.finditer(pattern, source): offsets.append(( match.start() - offsets[-1][1], match.end() - match.start() + offsets[-1][1] )) - self._uncommented[str(path)] = re.sub(COMMENTS_REGEX, "", source) + self._uncommented_source[str(path)] = re.sub(pattern, "", source) self._comment_offsets[str(path)] = offsets[::-1] def _get_contract_data(self, path): contracts = re.findall( - "((?:contract|library|interface)[\s\S]*?})\s*(?=contract|library|interface|$)", - self._uncommented[str(path)] + "((?:contract|library|interface)[^;{]*{[\s\S]*?})\s*(?=contract|library|interface|$)", + self._uncommented_source[str(path)] ) for source in contracts: type_, name, inherited = re.findall( @@ -56,22 +56,44 @@ def _get_contract_data(self, path): source )[0] inherited = set(i.strip() for i in inherited.split(', ') if i) - + offset = self._uncommented_source[str(path)].index(source) self._data[name] = { - 'sourcePath': path, + 'sourcePath': str(path), 'type': type_, 'inherited': inherited.union(re.findall("(?:;|{)\s*using *(\S*)(?= for)", source)), 'sha1': sha1(source.encode()).hexdigest(), - 'start': self.original_offset(path, self._uncommented[str(path)].index(source)), - 'stop': self.original_offset(path, self._uncommented[str(path)].index(source)+len(source)) + 'fn_offsets': [], + 'offset': ( + self._commented_offset(path, offset), + self._commented_offset(path, offset + len(source)) + ) } + if type_ == "interface": + continue + fn_offsets = [] + for idx, pattern in enumerate(( + # matches functions + "function\s*(\w*)[^{;]*{[\s\S]*?}(?=\s*function|\s*})", + # matches public variables + "(?:{|;)\s*(?!function)(\w[^;]*(?:public\s*constant|public)\s*(\w*)[^{;]*)(?=;)" + )): + for match in re.finditer(pattern, source): + fn_offsets.append(( + self._commented_offset(path, match.start(idx) + offset), + self._commented_offset(path, match.end(idx) + offset), + match.groups()[idx] + )) + self._data[name]['fn_offsets'] = sorted(fn_offsets, key=lambda k: k[0], reverse=True) + + def _commented_offset(self, path, offset): + return offset + next(i[1] for i in self._comment_offsets[str(path)] if i[0] <= offset) def _recursive_inheritance(self, inherited): final = set(inherited) for name in inherited: final |= self._recursive_inheritance(self._data[name]['inherited']) return final - + def get_hash(self, contract_name): return self._data[contract_name]['sha1'] @@ -80,9 +102,23 @@ def get_path(self, contract_name): def get_type(self, contract_name): return self._data[contract_name]['type'] - + + def get_fn(self, name, start, stop): + if name not in self._data: + name = next(( + k for k, v in self._data.items() if v['sourcePath'] == str(name) and + v['offset'][0] <= start <= stop <= v['offset'][1] + ), False) + if not name: + return False + offsets = self._data[name]['fn_offsets'] + if start < offsets[-1][0]: + return False + offset = next(i for i in offsets if start >= i[0]) + return False if stop >= offset[1] else offset[2] + def inheritance_map(self): - return dict((k, v['inherited'].copy()) for k,v in self._data.items()) + return dict((k, v['inherited'].copy()) for k, v in self._data.items()) def __getitem__(self, key): if key in self._data: @@ -95,6 +131,3 @@ def add_source(self, source): self._get_contract_data(path) self._string_iter += 1 return path - - def original_offset(self, path, offset): - return offset + next(i[1] for i in self._comment_offsets[str(path)] if i[0]<=offset) \ No newline at end of file From fe5b8521308dd057eb95229cd11e88f2dc911b37 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 18:46:35 +0300 Subject: [PATCH 17/72] use sources.get_fn --- brownie/network/transaction.py | 6 ++---- brownie/project/compiler.py | 13 +++---------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index a20beed6f..6772f24f4 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -339,10 +339,8 @@ def _evaluate_trace(self): } # jump 'i' is moving into an internal function if pc['jump'] == 'i': - source = contract._build['source'][pc['start']:pc['stop']] - if source[:7] not in ("library", "contrac") and "(" in source: - fn = source[:source.index('(')].split('.')[-1] - else: + fn = sources.get_fn(pc['contract'], pc['start'], pc['stop']) + if not fn: fn = last['fn'][-1] last['fn'].append(fn) # jump 'o' is coming out of an internal function diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index e47249608..9b6eacf3d 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -2,7 +2,6 @@ from hashlib import sha1 from pathlib import Path -import re import solcx from .sources import Sources @@ -82,7 +81,7 @@ def _compile_and_format(input_json): compiled = _generate_pcMap(compiled) result = {} - for filename, name in [(k,x) for k,v in compiled['contracts'].items() for x in v]: + for filename, name in [(k, x) for k, v in compiled['contracts'].items() for x in v]: data = compiled['contracts'][filename][name] evm = data['evm'] ref = [ @@ -247,14 +246,8 @@ def _isolate_functions(compiled): pcMap = compiled['pcMap'] fn_map = {} for op in _oplist(pcMap, "JUMPDEST"): - s = _get_source(op) - if s[:8] in ("contract", "library ", "interfac"): - continue - if s[:8] == "function": - fn = s[9:s.index('(')] - elif " public " in s: - fn = s[s.index(" public ")+8:].split(' =')[0].strip() - else: + fn = sources.get_fn(op['contract'], op['start'], op['stop']) + if not fn: continue if fn not in fn_map: fn_map[fn] = _base(op) From 5d73a2fc9312fa8324789cf66488479d064ae229 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 21:01:28 +0300 Subject: [PATCH 18/72] generate_coverage, working but ugly --- brownie/test/coverage.py | 84 +++++++++++++++++++++++++++++++++++----- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 3f4b1c2ab..75031d3c1 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -2,11 +2,12 @@ import json from pathlib import Path +import re -from brownie.project.build import Build +from brownie.project import Build, Sources build = Build() - +sources = Sources() def analyze_coverage(history): coverage_map = {} @@ -93,23 +94,23 @@ def analyze_coverage(history): def merge_coverage(coverage_files): - final = {} + coverage_eval = {} for filename in coverage_files: path = Path(filename) if not path.exists(): continue coverage = json.load(path.open())['coverage'] for key in list(coverage): - if key not in final: - final[key] = coverage.pop(key) + if key not in coverage_eval: + coverage_eval[key] = coverage.pop(key) continue for source, fn_name in [(k, x) for k, v in coverage[key].items() for x in v]: - f = final[key][source][fn_name] + f = coverage_eval[key][source][fn_name] c = coverage[key][source][fn_name] if not c['pct']: continue if f['pct'] == 1 or c['pct'] == 1: - final[key][source][fn_name] = {'pct': 1} + coverage_eval[key][source][fn_name] = {'pct': 1} continue _list_to_set(f,'line').update(c['line']) if 'true' in c: @@ -119,7 +120,7 @@ def merge_coverage(coverage_files): f['line'].add(i) f['true'].discard(i) f['false'].discard(i) - return final + return coverage_eval def _list_to_set(obj, key): @@ -127,4 +128,69 @@ def _list_to_set(obj, key): obj[key] = set(obj[key]) else: obj[key] = set() - return obj[key] \ No newline at end of file + return obj[key] + + +def generate_report(coverage_eval): + report = { + 'highlights':{}, + 'sha1':{} + } + for name, coverage in coverage_eval.items(): + report['highlights'][name] = {} + for path in coverage: + coverage_map = build[name]['coverageMap'][path] + report['highlights'][name][path] = [] + for key, fn, lines in [(k,v['fn'],v['line']) for k,v in coverage_map.items()]: + if coverage[path][key]['pct'] in (0, 1): + color = "green" if coverage[path][key]['pct'] else "red" + report['highlights'][name][path].append( + (fn['start'], fn['stop'], color, "") + ) + continue + for i, ln in enumerate(lines): + if i in coverage[path][key]['line']: + color = "green" + elif i in coverage[path][key]['true']: + color = "yellow" if _evaluate_branch(path, ln) else "orange" + elif i in coverage[path][key]['false']: + color = "orange" if _evaluate_branch(path, ln) else "yellow" + else: + color = "red" + report['highlights'][name][path].append( + (ln['start'], ln['stop'], color, "") + ) + return report + + +def _evaluate_branch(path, ln): + source = sources[path] + start, stop = ln['start'], ln['stop'] + try: + idx = _maxindex(source[:start]) + except: + return False + + # remove comments, strip whitespace + before = source[idx:start] + for pattern in ('\/\*[\s\S]*?\*\/', '\/\/[^\n]*'): + for i in re.findall(pattern, before): + before = before.replace(i, "") + before = before.strip("\n\t (") + + idx = source[stop:].index(';')+len(source[:stop]) + if idx <= stop: + return False + after = source[stop:idx].split() + after = next((i for i in after if i!=")"),after[0])[0] + if ( + (before[-2:] == "if" and after=="|") or + (before[:7] == "require" and after in (")","|")) + ): + return True + return False + + +def _maxindex(source): + comp = [i for i in [";", "}", "{"] if i in source] + return max([source.rindex(i) for i in comp])+1 \ No newline at end of file From bfe85f2b471d8b49d38f0327e4ad6037a2d896a3 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 22:16:19 +0300 Subject: [PATCH 19/72] source.get_fn returns offsets, refactor compiler _generate_coverageMap --- brownie/network/transaction.py | 5 ++-- brownie/project/compiler.py | 46 ++++++++++++++++------------------ brownie/project/sources.py | 16 ++++++------ 3 files changed, 32 insertions(+), 35 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 6772f24f4..def9ff7d4 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -35,6 +35,7 @@ web3 = Web3() sources = Sources() + class TransactionReceipt: '''Attributes and methods relating to a broadcasted transaction. @@ -217,7 +218,7 @@ def info(self): print(" Events In This Transaction\n --------------------------") for event in self.events: print(" "+color('bright yellow')+event.name+color()) - for k,v in event.items(): + for k, v in event.items(): print(" {0[key]}{1}{0}: {0[value]}{2}{0}".format( color, k, v )) @@ -339,7 +340,7 @@ def _evaluate_trace(self): } # jump 'i' is moving into an internal function if pc['jump'] == 'i': - fn = sources.get_fn(pc['contract'], pc['start'], pc['stop']) + fn = sources.get_fn(pc['contract'], pc['start'], pc['stop'])[0] if not fn: fn = last['fn'][-1] last['fn'].append(fn) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 9b6eacf3d..1a41f0c90 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -214,10 +214,7 @@ def _generate_coverageMap(build): } """ - fn_map = dict((x, {}) for x in build['allSourcePaths']) - - for i in _isolate_functions(build): - fn_map[i.pop('contract')][i.pop('method')] = {'fn': i, 'line': []} + fn_map = _isolate_functions(build) line_map = _isolate_lines(build) if not line_map: return {} @@ -241,30 +238,29 @@ def _generate_coverageMap(build): return fn_map -def _isolate_functions(compiled): +def _isolate_functions(build_json): '''Identify function level coverage map items.''' - pcMap = compiled['pcMap'] - fn_map = {} - for op in _oplist(pcMap, "JUMPDEST"): - fn = sources.get_fn(op['contract'], op['start'], op['stop']) - if not fn: - continue - if fn not in fn_map: - fn_map[fn] = _base(op) - fn_map[fn]['method'] = fn - fn_map[fn]['pc'].add(op['pc']) - - fn_map = _sort(fn_map.values()) - if not fn_map: - return [] - for op in _oplist(pcMap): - try: - f = _next(fn_map, op) - except StopIteration: + pcMap = build_json['pcMap'] + fn_map = dict((i, {}) for i in build_json['allSourcePaths']) + for op in pcMap: + if not op['contract']: continue - if op['stop'] > f['stop']: + fn, start, stop = sources.get_fn(op['contract'], op['start'], op['stop']) + if not fn: continue - f['pc'].add(op['pc']) + if fn not in fn_map[op['contract']]: + fn_map[op['contract']][fn] = { + 'fn': { + 'contract': op['contract'], + 'start': start, + 'stop': stop, + 'pc': set([op['pc']]), + 'jump': False + }, + 'line': [] + } + fn_map[op['contract']][fn]['fn']['pc'].add(op['pc']) + return fn_map diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 15b591d0f..d321d5aa3 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -79,11 +79,11 @@ def _get_contract_data(self, path): )): for match in re.finditer(pattern, source): fn_offsets.append(( + match.groups()[idx], self._commented_offset(path, match.start(idx) + offset), - self._commented_offset(path, match.end(idx) + offset), - match.groups()[idx] + self._commented_offset(path, match.end(idx) + offset) )) - self._data[name]['fn_offsets'] = sorted(fn_offsets, key=lambda k: k[0], reverse=True) + self._data[name]['fn_offsets'] = sorted(fn_offsets, key=lambda k: k[1], reverse=True) def _commented_offset(self, path, offset): return offset + next(i[1] for i in self._comment_offsets[str(path)] if i[0] <= offset) @@ -110,12 +110,12 @@ def get_fn(self, name, start, stop): v['offset'][0] <= start <= stop <= v['offset'][1] ), False) if not name: - return False + return (False, -1, -1) offsets = self._data[name]['fn_offsets'] - if start < offsets[-1][0]: - return False - offset = next(i for i in offsets if start >= i[0]) - return False if stop >= offset[1] else offset[2] + if start < offsets[-1][1]: + return (False, -1, -1) + offset = next(i for i in offsets if start >= i[1]) + return (False, -1, -1) if stop >= offset[2] else offset def inheritance_map(self): return dict((k, v['inherited'].copy()) for k, v in self._data.items()) From 942534edff219e2c5be44a525691cd1b7e96be59 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 9 May 2019 22:19:56 +0300 Subject: [PATCH 20/72] use coverage.generate_report in GUI --- brownie/gui/root.py | 77 ++++++--------------------------------------- 1 file changed, 10 insertions(+), 67 deletions(-) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 0561a2c2e..36d7943a3 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 from pathlib import Path -import re import threading import tkinter as tk @@ -13,11 +12,12 @@ from .styles import set_style from brownie.project.build import Build -from brownie.test.coverage import merge_coverage +from brownie.test.coverage import merge_coverage, generate_report from brownie._config import CONFIG build = Build() + class Root(tk.Tk): _active = threading.Event() @@ -33,96 +33,39 @@ def __init__(self): super().__init__(className="Opcode Viewer") self.bind("", lambda k: self.destroy()) - path = Path(CONFIG['folders']['project']) - self.coverage_folder = path.joinpath('build/coverage') - self.note = TextBook(self) self.note.pack(side="left") frame = ttk.Frame(self) frame.pack(side="right", expand="true", fill="y") - + self.tree = ListView(self, frame, (("pc", 80), ("opcode", 200)), height=30) self.tree.pack(side="bottom") self.combo = SelectContract(self, frame) self.combo.pack(side="top", expand="true", fill="x") + path = Path(CONFIG['folders']['project']).joinpath('build/coverage') + coverage_eval = merge_coverage(path.glob('**/*.json')) + self._coverage_report = generate_report(coverage_eval)['highlights'] self._show_coverage = False self.bind("c", self._toggle_coverage) set_style(self) def _toggle_coverage(self, event): active = self.combo.get() - if not active: + if not active or active not in self._coverage_report: return if self._show_coverage: self.note.unmark_all('green', 'red', 'yellow', 'orange') self._show_coverage = False return - frame_path = self.note.active_frame()._path - coverage_files = self.coverage_folder.glob('**/*.json') - try: - coverage = merge_coverage(coverage_files)[active][frame_path] - except KeyError: - return - source = build[active]['source'] - coverage_map = build[active]['coverageMap'][frame_path] - label = frame_path.split('/')[-1] + for path, item in [(k, x) for k, v in self._coverage_report[active].items() for x in v]: + label = Path(path).name + self.note.mark(label, item[2], item[0], item[1]) self._show_coverage = True - for key, fn, lines in [(k,v['fn'],v['line']) for k,v in coverage_map.items()]: - if coverage[key]['pct'] in (0, 1): - self.note.mark( - label, - "green" if coverage[key]['pct'] else "red", - fn['start'], - fn['stop'] - ) - continue - for i, ln in enumerate(lines): - if i in coverage[key]['line']: - tag = "green" - elif i in coverage[key]['true']: - tag = "yellow" if _evaluate_branch(source, ln) else "orange" - elif i in coverage[key]['false']: - tag = "orange" if _evaluate_branch(source, ln) else "yellow" - else: - tag = "red" - self.note.mark(label, tag, ln['start'], ln['stop']) def destroy(self): super().destroy() self.quit() self._active.clear() - - -def _evaluate_branch(source, ln): - start, stop = ln['start'], ln['stop'] - try: - idx = _maxindex(source[:start]) - except: - return False - - # remove comments, strip whitespace - before = source[idx:start] - for pattern in ('\/\*[\s\S]*?\*\/', '\/\/[^\n]*'): - for i in re.findall(pattern, before): - before = before.replace(i, "") - before = before.strip("\n\t (") - - idx = source[stop:].index(';')+len(source[:stop]) - if idx <= stop: - return False - after = source[stop:idx].split() - after = next((i for i in after if i!=")"),after[0])[0] - if ( - (before[-2:] == "if" and after=="|") or - (before[:7] == "require" and after in (")","|")) - ): - return True - return False - - -def _maxindex(source): - comp = [i for i in [";", "}", "{"] if i in source] - return max([source.rindex(i) for i in comp])+1 From c2b16173188b280e74d0e251fe4927650d562c79 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 00:51:27 +0300 Subject: [PATCH 21/72] simplify coverageMap --- brownie/project/build.py | 3 +- brownie/project/compiler.py | 72 +++++++++++++------------------------ 2 files changed, 26 insertions(+), 49 deletions(-) diff --git a/brownie/project/build.py b/brownie/project/build.py index beda97e36..67ee4021b 100644 --- a/brownie/project/build.py +++ b/brownie/project/build.py @@ -63,7 +63,7 @@ def _load(self): data, self._path.joinpath("{}.json".format(name)).open('w'), sort_keys=True, - indent=4, + indent=2, default=sorted ) self._build.update(build_json) @@ -78,6 +78,7 @@ def _load_build_data(self): set(BUILD_KEYS).issubset(build_json) and Path(build_json['sourcePath']).exists() ): + build_json['pcMap'] = dict((int(k), v) for k, v in build_json['pcMap'].items()) self._build[path.stem] = build_json continue except json.JSONDecodeError: diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 1a41f0c90..c6c75d3e2 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -115,6 +115,10 @@ def _compile_and_format(input_json): 'pcMap': evm['pcMap'] } result[name]['coverageMap'] = _generate_coverageMap(result[name]) + + # TODO - pcMap should not be transformed into a dict midway through + # compiling, choose one or the other! + result[name]['pcMap'] = dict((i.pop('pc'), i) for i in evm['pcMap']) return result @@ -138,7 +142,6 @@ def _generate_pcMap(compiled): if not bytecode['object']: compiled['contracts'][filename][name]['evm']['pcMap'] = [] continue - pcMap = [] opcodes = bytecode['opcodes'] source_map = bytecode['sourceMap'] while True: @@ -154,14 +157,15 @@ def _generate_pcMap(compiled): last = source_map.split(';')[0].split(':') for i in range(3): last[i] = int(last[i]) - pcMap.append({ + pcMap = [{ 'start': last[0], 'stop': last[0]+last[1], 'op': opcodes.pop(), 'contract': id_map[last[2]], 'jump': last[3], - 'pc': 0 - }) + 'pc': 0, + 'fn': False, + }] pcMap[0]['value'] = opcodes.pop() for value in source_map.split(';')[1:]: pc += 1 @@ -173,13 +177,15 @@ def _generate_pcMap(compiled): value[i] = int(value[i] or last[i]) value[3] = value[3] or last[3] last = value + contract = id_map[last[2]] if last[2] != -1 else False pcMap.append({ 'start': last[0], 'stop': last[0]+last[1], 'op': opcodes.pop(), - 'contract': id_map[last[2]] if last[2] != -1 else False, + 'contract': contract, 'jump': last[3], - 'pc': pc + 'pc': pc, + 'fn': sources.get_fn(contract, last[0], last[0]+last[1])[0], }) if opcodes[-1][:2] == "0x": pcMap[-1]['value'] = opcodes.pop() @@ -214,54 +220,24 @@ def _generate_coverageMap(build): } """ - fn_map = _isolate_functions(build) line_map = _isolate_lines(build) if not line_map: return {} - # future me - i'm sorry for this line - for source, fn_name, fn in [(k, x, v[x]['fn']) for k, v in fn_map.items() for x in v]: - for ln in [ - i for i in line_map if i['contract'] == source and - i['start'] == fn['start'] and i['stop'] == fn['stop'] - ]: - # remove duplicate mappings - line_map.remove(ln) - for ln in [ - i for i in line_map if i['contract'] == source and - i['start'] >= fn['start'] and i['stop'] <= fn['stop'] - ]: - # apply method names to line mappings - line_map.remove(ln) - fn_map[ln.pop('contract')][fn_name]['line'].append(ln) - fn_map[source][fn_name]['total'] = sum([1 if not i['jump'] else 2 for i in fn_map[source][fn_name]['line']]) - return fn_map - - -def _isolate_functions(build_json): - '''Identify function level coverage map items.''' - pcMap = build_json['pcMap'] - fn_map = dict((i, {}) for i in build_json['allSourcePaths']) - for op in pcMap: - if not op['contract']: - continue - fn, start, stop = sources.get_fn(op['contract'], op['start'], op['stop']) + final = dict((i, {}) for i in set(i['contract'] for i in line_map)) + pcMap = dict((i['pc'], i) for i in build['pcMap']) + for i in line_map: + fn = sources.get_fn(i['contract'], i['start'], i['stop'])[0] if not fn: continue - if fn not in fn_map[op['contract']]: - fn_map[op['contract']][fn] = { - 'fn': { - 'contract': op['contract'], - 'start': start, - 'stop': stop, - 'pc': set([op['pc']]), - 'jump': False - }, - 'line': [] - } - fn_map[op['contract']][fn]['fn']['pc'].add(op['pc']) - - return fn_map + final[i['contract']].setdefault(fn, []).append({ + 'jump': i['jump'], + 'start': i['start'], + 'stop': i['stop'] + }) + for pc in i['pc']: + pcMap[pc]['coverageIndex'] = len(final[i['contract']][fn]) - 1 + return final def _isolate_lines(compiled): From b4e6847e8f1c642c2d8d38371fba3d584f2b0cc4 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 03:21:57 +0300 Subject: [PATCH 22/72] refactor analyze_coverage based on new coverageMap --- brownie/cli/test.py | 2 +- brownie/test/coverage.py | 108 ++++++++++++++++++--------------------- 2 files changed, 50 insertions(+), 60 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index eb16585a1..fb42c38c0 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -240,7 +240,7 @@ def main(): coverage_eval, coverage_json.open('w'), sort_keys=True, - indent=4, + indent=2, default=sorted ) except KeyboardInterrupt: diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 75031d3c1..fb366c019 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -10,86 +10,76 @@ sources = Sources() def analyze_coverage(history): - coverage_map = {} + build_json = {} coverage_eval = {} + coverage_map = {} + pcMap = {} for tx in history: if not tx.receiver: continue + tx_eval = {} for i in range(len(tx.trace)): t = tx.trace[i] pc = t['pc'] name = t['contractName'] - source = t['source']['filename'] - if not name or not source: + path = t['source']['filename'] + if not name or not path: continue - if name not in coverage_map: + + # prevent repeated requests to build object + if name not in pcMap: + pcMap[name] = build[name]['pcMap'] coverage_map[name] = build[name]['coverageMap'] coverage_eval[name] = dict((i, {}) for i in coverage_map[name]) - try: - # find the function map item and record the tx - fn = next(v for k, v in coverage_map[name][source].items() if pc in v['fn']['pc']) - fn['fn'].setdefault('tx',set()).add(tx) - if t['op'] != "JUMPI": - # if not a JUMPI, find the line map item and record - ln = next(i for i in fn['line'] if pc in i['pc']) - for key in ('tx', 'true', 'false'): - ln.setdefault(key, set()) - ln['tx'].add(tx) - continue - # if a JUMPI, we need to have hit the jump pc AND a related opcode - ln = next(i for i in fn['line'] if pc == i['jump']) - for key in ('tx', 'true', 'false'): - ln.setdefault(key, set()) - if tx not in ln['tx']: + if name not in tx_eval: + tx_eval[name] = dict((i, {}) for i in coverage_map[name]) + + fn = pcMap[name][pc]['fn'] + if not fn: + continue + + coverage_eval[name][path].setdefault(fn, {'tx': set(), 'true': set(), 'false': set()}) + tx_eval[name][path].setdefault(fn, set()) + if t['op'] != "JUMPI": + if 'coverageIndex' not in pcMap[name][pc]: continue - # if the next opcode is not pc+1, the JUMPI was executed truthy - key = 'false' if tx.trace[i+1]['pc'] == pc+1 else 'true' - ln[key].add(tx) - # pc didn't exist in map - except StopIteration: + # if not a JUMPI, record at the coverage map index + idx = pcMap[name][pc]['coverageIndex'] + if coverage_map[name][path][fn][idx]['jump']: + tx_eval[name][path][fn].add(pcMap[name][pc]['coverageIndex']) + else: + coverage_eval[name][path][fn]['tx'].add(pcMap[name][pc]['coverageIndex']) + continue + # if a JUMPI, check that we hit the jump AND the related coverage map + idx = coverage_map[name][path][fn].index(next(i for i in coverage_map[name][path][fn] if i['jump']==pc)) + if idx not in tx_eval[name][path][fn] or idx in coverage_eval[name][path][fn]['tx']: continue + key = ('false', 'true') if tx.trace[i+1]['pc'] == pc+1 else ('true', 'false') + # if the conditional evaluated both ways, record on the main eval dict + if idx not in coverage_eval[name][path][fn][key[1]]: + coverage_eval[name][path][fn][key[0]].add(idx) + continue + coverage_eval[name][path][fn][key[1]].discard(idx) + coverage_eval[name][path][fn]['tx'].add(idx) + # evaluate coverage %'s for contract, source, fn_name, maps in [(k,w,y,z) for k,v in coverage_map.items() for w,x in v.items() for y,z in x.items()]: - fn = maps['fn'] - if 'tx' not in fn or not fn['tx']: - coverage_eval[contract][source][fn_name] = {'pct':0} - continue - for ln in maps['line']: - if 'tx' not in ln: - ln['count'] = 0 - continue - if ln['jump']: - ln['jump'] = [len(ln['true']), len(ln['false'])] - ln['count'] = len(ln['tx']) - if not [i for i in maps['line'] if i['count']]: - coverage_eval[contract][source][fn_name] = {'pct':0} + if fn_name not in coverage_eval[contract][source]: + coverage_eval[contract][source][fn_name] = {'pct': 0} continue + total = len([i for i in maps if i['jump']])*2 + len([i for i in maps if not i['jump']]) + result = coverage_eval[contract][source][fn_name] count = 0 - coverage = { - 'line': set(), - 'true': set(), - 'false': set() - } - for c,i in enumerate(maps['line']): - if not i['count']: + for idx, item in enumerate(maps): + if idx in result['tx']: + count += 2 if item['jump'] else 1 continue - if not i['jump'] or False not in i['jump']: - coverage['line'].add(c) - count += 2 if i['jump'] else 1 + if not item['jump']: continue - if i['jump'][0]: - coverage['true'].add(c) + if idx in result['true'] or idx in result['false']: count += 1 - if i['jump'][1]: - coverage['false'].add(c) - count += 1 - if count == maps['total']: - coverage_eval[contract][source][fn_name] = {'pct': 1} - else: - coverage['pct'] = round(count/maps['total'], 4) - coverage_eval[contract][source][fn_name] = coverage - + result['pct'] = round(count / total, 4) return coverage_eval From 47c4870de69c5e892881b1c1773f704d15bfedce Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 04:14:07 +0300 Subject: [PATCH 23/72] update merge_coverage --- brownie/test/coverage.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index fb366c019..23e8df744 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -80,37 +80,36 @@ def analyze_coverage(history): if idx in result['true'] or idx in result['false']: count += 1 result['pct'] = round(count / total, 4) + if result['pct'] == 1: + coverage_eval[contract][source][fn_name] = {'pct': 1} return coverage_eval def merge_coverage(coverage_files): - coverage_eval = {} + merged_eval = {} for filename in coverage_files: path = Path(filename) if not path.exists(): continue coverage = json.load(path.open())['coverage'] - for key in list(coverage): - if key not in coverage_eval: - coverage_eval[key] = coverage.pop(key) + for contract_name in list(coverage): + if contract_name not in merged_eval: + merged_eval[contract_name] = coverage.pop(contract_name) continue - for source, fn_name in [(k, x) for k, v in coverage[key].items() for x in v]: - f = coverage_eval[key][source][fn_name] - c = coverage[key][source][fn_name] - if not c['pct']: + for source, fn_name in [(k, x) for k, v in coverage[contract_name].items() for x in v]: + f = merged_eval[contract_name][source][fn_name] + c = coverage[contract_name][source][fn_name] + if not c['pct'] or f == c: continue if f['pct'] == 1 or c['pct'] == 1: - coverage_eval[key][source][fn_name] = {'pct': 1} + merged_eval[contract_name][source][fn_name] = {'pct': 1} continue - _list_to_set(f,'line').update(c['line']) - if 'true' in c: - _list_to_set(f, "true").update(c['true']) - _list_to_set(f, "false").update(c['false']) - for i in f['true'].intersection(f['false']): - f['line'].add(i) - f['true'].discard(i) - f['false'].discard(i) - return coverage_eval + f['true'] += c['true'] + f['false'] += c['false'] + f['tx'] = list(set(f['tx']+c['tx']+[i for i in f['true'] if i in f['false']])) + f['true'] = list(set([i for i in f['true'] if i not in f['tx']])) + f['false'] = list(set([i for i in f['false'] if i not in f['tx']])) + return merged_eval def _list_to_set(obj, key): From 6b5042d8de3d9180bdc8839e23a69b038a14f0c5 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 04:35:13 +0300 Subject: [PATCH 24/72] calculate_pct --- brownie/test/coverage.py | 52 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 23e8df744..7e5d30d3e 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -61,27 +61,35 @@ def analyze_coverage(history): continue coverage_eval[name][path][fn][key[1]].discard(idx) coverage_eval[name][path][fn]['tx'].add(idx) + return calculate_pct(coverage_eval) - # evaluate coverage %'s - for contract, source, fn_name, maps in [(k,w,y,z) for k,v in coverage_map.items() for w,x in v.items() for y,z in x.items()]: - if fn_name not in coverage_eval[contract][source]: - coverage_eval[contract][source][fn_name] = {'pct': 0} - continue - total = len([i for i in maps if i['jump']])*2 + len([i for i in maps if not i['jump']]) - result = coverage_eval[contract][source][fn_name] - count = 0 - for idx, item in enumerate(maps): - if idx in result['tx']: - count += 2 if item['jump'] else 1 +def calculate_pct(coverage_eval): + for name in coverage_eval: + coverage_map = build[name]['coverageMap'] + for path, fn_name in [(k,x) for k,v in coverage_map.items() for x in v]: + result = coverage_eval[name][path] + if fn_name not in result: + result[fn_name] = {'pct': 0} continue - if not item['jump']: + if 'pct' in result[fn_name] and result[fn_name]['pct'] in (0, 1): + result[fn_name] = {'pct': result[fn_name]['pct']} continue - if idx in result['true'] or idx in result['false']: - count += 1 - result['pct'] = round(count / total, 4) - if result['pct'] == 1: - coverage_eval[contract][source][fn_name] = {'pct': 1} + result = result[fn_name] + count = 0 + maps = coverage_map[path][fn_name] + total = len([i for i in maps if i['jump']])*2 + len([i for i in maps if not i['jump']]) + for idx, item in enumerate(maps): + if idx in result['tx']: + count += 2 if item['jump'] else 1 + continue + if not item['jump']: + continue + if idx in result['true'] or idx in result['false']: + count += 1 + result['pct'] = round(count / total, 4) + if result['pct'] == 1: + coverage_eval[name][path][fn_name] = {'pct': 1} return coverage_eval @@ -109,15 +117,7 @@ def merge_coverage(coverage_files): f['tx'] = list(set(f['tx']+c['tx']+[i for i in f['true'] if i in f['false']])) f['true'] = list(set([i for i in f['true'] if i not in f['tx']])) f['false'] = list(set([i for i in f['false'] if i not in f['tx']])) - return merged_eval - - -def _list_to_set(obj, key): - if key in obj: - obj[key] = set(obj[key]) - else: - obj[key] = set() - return obj[key] + return calculate_pct(merged_eval) def generate_report(coverage_eval): From 985ed6da487bfe15dc0ce9f3c77abaca1a17167b Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 04:44:44 +0300 Subject: [PATCH 25/72] get function name from pcMap --- brownie/network/transaction.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index def9ff7d4..24059160c 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -340,10 +340,7 @@ def _evaluate_trace(self): } # jump 'i' is moving into an internal function if pc['jump'] == 'i': - fn = sources.get_fn(pc['contract'], pc['start'], pc['stop'])[0] - if not fn: - fn = last['fn'][-1] - last['fn'].append(fn) + last['fn'].append(pc['fn'] or last['fn'][-1]) # jump 'o' is coming out of an internal function elif pc['jump'] == "o" and len(['fn']) > 1: del last['fn'][-1] From 1688b646088f38a5401658e58e9169443b74cef6 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 04:45:00 +0300 Subject: [PATCH 26/72] deepcopy no longer needed --- brownie/network/contract.py | 3 --- brownie/project/build.py | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 010a969bf..8bfaca0d6 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -62,9 +62,6 @@ class ContractContainer(_ContractBase): def __init__(self, build): self.tx = None self.bytecode = build['bytecode'] - # convert pcMap to dict to speed transaction stack traces - if type(build['pcMap']) is list: - build['pcMap'] = dict((i.pop('pc'), i) for i in build['pcMap']) super().__init__(build) self.deploy = ContractConstructor(self, self._name) diff --git a/brownie/project/build.py b/brownie/project/build.py index 67ee4021b..3698eaabd 100644 --- a/brownie/project/build.py +++ b/brownie/project/build.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 import ast -from copy import deepcopy from hashlib import sha1 import importlib.util import json @@ -121,10 +120,10 @@ def _check_coverage_hashes(self): break def __getitem__(self, contract_name): - return deepcopy(self._build[contract_name.replace('.json','')]) + return self._build[contract_name.replace('.json','')] def items(self): - return deepcopy(self._build).items() + return self._build.items() def get_ast_hash(script_path): From efd3ed114ace775c5b81ec13d90da5ee635d252a Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 05:09:34 +0300 Subject: [PATCH 27/72] get_fn_offsets --- brownie/project/sources.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index d321d5aa3..8bd32c519 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -117,6 +117,14 @@ def get_fn(self, name, start, stop): offset = next(i for i in offsets if start >= i[1]) return (False, -1, -1) if stop >= offset[2] else offset + def get_fn_offset(self, name, fn_name): + if name not in self._data: + name = next( + k for k, v in self._data.items() if v['sourcePath'] == str(name) and + fn_name in [i[0] for i in v['fn_offsets']] + ) + return next(i for i in self._data[name]['fn_offsets'] if i[0] == fn_name)[1:3] + def inheritance_map(self): return dict((k, v['inherited'].copy()) for k, v in self._data.items()) From 715ad0887e0101024297375b24f07a47858dcd8d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 14:14:47 +0300 Subject: [PATCH 28/72] update generate_report --- brownie/test/coverage.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 7e5d30d3e..7f63dd72b 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -130,19 +130,20 @@ def generate_report(coverage_eval): for path in coverage: coverage_map = build[name]['coverageMap'][path] report['highlights'][name][path] = [] - for key, fn, lines in [(k,v['fn'],v['line']) for k,v in coverage_map.items()]: - if coverage[path][key]['pct'] in (0, 1): - color = "green" if coverage[path][key]['pct'] else "red" + for fn_name, lines in coverage_map.items(): + if coverage[path][fn_name]['pct'] in (0, 1): + color = "green" if coverage[path][fn_name]['pct'] else "red" + start, stop = sources.get_fn_offset(path, fn_name) report['highlights'][name][path].append( - (fn['start'], fn['stop'], color, "") + (start, stop, color, "") ) continue for i, ln in enumerate(lines): - if i in coverage[path][key]['line']: + if i in coverage[path][fn_name]['tx']: color = "green" - elif i in coverage[path][key]['true']: + elif i in coverage[path][fn_name]['true']: color = "yellow" if _evaluate_branch(path, ln) else "orange" - elif i in coverage[path][key]['false']: + elif i in coverage[path][fn_name]['false']: color = "orange" if _evaluate_branch(path, ln) else "yellow" else: color = "red" From bd3805099e46dd6b3baf0b6e2f1495d8a39feb9e Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 14:25:39 +0300 Subject: [PATCH 29/72] gui updates --- brownie/gui/listview.py | 4 ++-- brownie/gui/select.py | 22 ++++++++++------------ brownie/gui/textbook.py | 11 +++++++---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/brownie/gui/listview.py b/brownie/gui/listview.py index 311ba0ecb..60789eadd 100755 --- a/brownie/gui/listview.py +++ b/brownie/gui/listview.py @@ -83,7 +83,7 @@ def _select_bind(self, event): if not pcMap[pc]['contract']: note.active_frame().clear_highlight() return - note.set_active(pcMap[pc]['contract'].split('/')[-1]) + note.set_active(pcMap[pc]['contract']) note.active_frame().highlight(pcMap[pc]['start'], pcMap[pc]['stop']) def _seek(self, event): @@ -110,7 +110,7 @@ def _show_scope(self, event): return for key, value in sorted(self._parent.pcMap.items(), key= lambda k: int(k[0])): if ( - not value['contract'] or value['contract']!=pc['contract'] or + not value['contract'] or value['contract'] != pc['contract'] or value['start'] < pc['start'] or value['stop']>pc['stop'] ): self.detach(key) diff --git a/brownie/gui/select.py b/brownie/gui/select.py index 79c421ea6..95d4c4d17 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +from copy import deepcopy from tkinter import ttk from brownie.project.build import Build @@ -24,24 +25,21 @@ def _select(self, event): self._parent.note.set_visible([]) build_json = build[self.get()] self.selection_clear() - for contract in sorted(set( - i['contract'].split('/')[-1] - for i in build_json['pcMap'] if i['contract'] - )): + for contract in build_json['allSourcePaths']: self._parent.note.show(contract) - first = build_json['pcMap'][0].copy() - self._parent.note.set_active(first['contract'].split('/')[-1]) + pcMap = deepcopy(build_json['pcMap']) + self._parent.note.set_active(build_json['sourcePath']) self._parent.tree.delete_all() - for op in build_json['pcMap']: + for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)[1:]]: if ( - op['contract'] == first['contract'] and - op['start'] == first['start'] and - op['stop'] == first['stop'] + op['contract'] == pcMap[0]['contract'] and + op['start'] == pcMap[0]['start'] and + op['stop'] == pcMap[0]['stop'] ): op['contract'] = None if op['contract']: tag = "{0[start]}:{0[stop]}:{0[contract]}".format(op) else: tag = "NoSource" - self._parent.tree.insert([str(op['pc']), op['op']], [tag, op['op']]) - self._parent.pcMap = dict((str(i.pop('pc')), i) for i in build_json['pcMap']) + self._parent.tree.insert([str(pc), op['op']], [tag, op['op']]) + self._parent.pcMap = dict((str(k),v) for k,v in pcMap.items()) diff --git a/brownie/gui/textbook.py b/brownie/gui/textbook.py index e3007bfe6..cfd744e25 100755 --- a/brownie/gui/textbook.py +++ b/brownie/gui/textbook.py @@ -25,6 +25,7 @@ def __init__(self, root): self.add(path) def add(self, path): + path = Path(path) label = path.name if label in [i._label for i in self._frames]: return @@ -37,6 +38,7 @@ def add(self, path): self._frames.append(frame) def get_frame(self, label): + label = Path(label).name return next(i for i in self._frames if i._label == label) def hide(self, label): @@ -46,6 +48,7 @@ def hide(self, label): frame._visible = False def show(self, label): + label = Path(label).name frame = next(i for i in self._frames if i._label == label) if frame._visible: return @@ -53,6 +56,7 @@ def show(self, label): super().add(frame, text=" {} ".format(label)) def set_visible(self, labels): + labels = [Path(i).name for i in labels] for label in [i._label for i in self._frames]: if label in labels: self.show(label) @@ -166,10 +170,9 @@ def __init__(self, root, text): for k,v in TEXT_COLORS.items(): self._text.tag_config(k, **v) - for pattern in ('\/\*[\s\S]*?\*\/', '\/\/[^\n]*'): - for i in re.findall(pattern, text): - idx = text.index(i) - self.tag_add('comment',idx,idx+len(i)) + pattern = "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + for match in re.finditer(pattern, text): + self.tag_add('comment', match.start(), match.end()) self._line_no.insert(1.0, '\n'.join(str(i) for i in range(1, text.count('\n')+2))) self._line_no.tag_configure("justify", justify="right") From 5f61e5c5f454e37d1224422f453fc31b2b5c9454 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 15:26:10 +0300 Subject: [PATCH 30/72] pcMap is always a dict --- brownie/project/compiler.py | 83 +++++++++++++++---------------------- 1 file changed, 33 insertions(+), 50 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index c6c75d3e2..30a8a2c35 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -97,7 +97,7 @@ def _compile_and_format(input_json): ) result[name] = { 'abi': data['abi'], - 'allSourcePaths': sorted(set(i['contract'] for i in evm['pcMap'] if i['contract'])), + 'allSourcePaths': sorted(set(v['contract'] for v in evm['pcMap'].values() if v['contract'])), 'ast': compiled['sources'][filename]['ast'], 'bytecode': evm['bytecode']['object'], 'bytecodeSha1': sha1(evm['bytecode']['object'][:-68].encode()).hexdigest(), @@ -115,10 +115,6 @@ def _compile_and_format(input_json): 'pcMap': evm['pcMap'] } result[name]['coverageMap'] = _generate_coverageMap(result[name]) - - # TODO - pcMap should not be transformed into a dict midway through - # compiling, choose one or the other! - result[name]['pcMap'] = dict((i.pop('pc'), i) for i in evm['pcMap']) return result @@ -157,20 +153,19 @@ def _generate_pcMap(compiled): last = source_map.split(';')[0].split(':') for i in range(3): last[i] = int(last[i]) - pcMap = [{ + pcMap = {0: { 'start': last[0], 'stop': last[0]+last[1], 'op': opcodes.pop(), 'contract': id_map[last[2]], 'jump': last[3], - 'pc': 0, - 'fn': False, - }] + 'fn': False + }} pcMap[0]['value'] = opcodes.pop() for value in source_map.split(';')[1:]: + if pcMap[pc]['op'][:4] == "PUSH": + pc += int(pcMap[pc]['op'][4:]) pc += 1 - if pcMap[-1]['op'][:4] == "PUSH": - pc += int(pcMap[-1]['op'][4:]) if value: value = (value+":::").split(':')[:4] for i in range(3): @@ -178,17 +173,16 @@ def _generate_pcMap(compiled): value[3] = value[3] or last[3] last = value contract = id_map[last[2]] if last[2] != -1 else False - pcMap.append({ + pcMap[pc] = { 'start': last[0], 'stop': last[0]+last[1], 'op': opcodes.pop(), 'contract': contract, 'jump': last[3], - 'pc': pc, - 'fn': sources.get_fn(contract, last[0], last[0]+last[1])[0], - }) + 'fn': sources.get_fn(contract, last[0], last[0]+last[1])[0] + } if opcodes[-1][:2] == "0x": - pcMap[-1]['value'] = opcodes.pop() + pcMap[pc]['value'] = opcodes.pop() compiled['contracts'][filename][name]['evm']['pcMap'] = pcMap return compiled @@ -225,7 +219,6 @@ def _generate_coverageMap(build): return {} final = dict((i, {}) for i in set(i['contract'] for i in line_map)) - pcMap = dict((i['pc'], i) for i in build['pcMap']) for i in line_map: fn = sources.get_fn(i['contract'], i['start'], i['stop'])[0] if not fn: @@ -236,7 +229,7 @@ def _generate_coverageMap(build): 'stop': i['stop'] }) for pc in i['pc']: - pcMap[pc]['coverageIndex'] = len(final[i['contract']][fn]) - 1 + build['pcMap'][pc]['coverageIndex'] = len(final[i['contract']][fn]) - 1 return final @@ -251,7 +244,7 @@ def _isolate_lines(compiled): line_map = {} # find all the JUMPI opcodes - for i in [pcMap.index(i) for i in _oplist(pcMap, "JUMPI")]: + for i in [k for k,v in pcMap.items() if v['contract'] and v['op']=="JUMPI"]: op = pcMap[i] if op['contract'] not in line_map: line_map[op['contract']] = [] @@ -261,39 +254,46 @@ def _isolate_lines(compiled): try: # JUMPI is to the closest previous opcode that has # a different source offset and is not a JUMPDEST - req = next( - x for x in pcMap[i-2::-1] if x['contract'] and - x['op'] != "JUMPDEST" and - x['start'] + x['stop'] != op['start'] + op['stop'] + pc = next( + x for x in range(i - 4, 0, -1) if x in pcMap and + pcMap[x]['contract'] and pcMap[x]['op'] != "JUMPDEST" and + (pcMap[x]['start'], pcMap[x]['stop']) != (op['start'], op['stop']) ) except StopIteration: continue - line_map[op['contract']].append(_base(req)) - line_map[op['contract']][-1].update({'jump': op['pc']}) + line_map[op['contract']].append(_base(pc, pcMap[pc])) + line_map[op['contract']][-1].update({'jump': i}) # analyze all the opcodes - for op in _oplist(pcMap): + for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)]: # ignore code that spans multiple lines - if ';' in _get_source(op): + if not op['contract'] or ';' in _get_source(op): continue if op['contract'] not in line_map: line_map[op['contract']] = [] # find existing related coverage map item, make a new one if none exists try: - ln = _next(line_map[op['contract']], op) + ln = next( + i for i in line_map[op['contract']] if + i['contract'] == op['contract'] and + i['start'] <= op['start'] < i['stop'] + ) except StopIteration: - line_map[op['contract']].append(_base(op)) + line_map[op['contract']].append(_base(pc, op)) continue if op['stop'] > ln['stop']: # if coverage map item is a jump, do not modify the source offsets if ln['jump']: continue ln['stop'] = op['stop'] - ln['pc'].add(op['pc']) + ln['pc'].add(pc) # sort the coverage map and merge overlaps where possible for contract in line_map: - line_map[contract] = _sort(line_map[contract]) + line_map[contract] = sorted( + line_map[contract], + key=lambda k: (k['contract'], k['start'], k['stop']) + ) ln_map = line_map[contract] i = 0 while True: @@ -322,28 +322,11 @@ def _get_source(op): return sources[op['contract']][op['start']:op['stop']] -def _next(coverage_map, op): - '''Given a coverage map and an item from pcMap, returns the related - coverage map item (based on source offset overlap).''' - return next( - i for i in coverage_map if i['contract'] == op['contract'] and - i['start'] <= op['start'] < i['stop'] - ) - - -def _sort(list_): - return sorted(list_, key=lambda k: (k['contract'], k['start'], k['stop'])) - - -def _oplist(pcMap, op=None): - return [i for i in pcMap if i['contract'] and (not op or op == i['op'])] - - -def _base(op): +def _base(pc, op): return { 'contract': op['contract'], 'start': op['start'], 'stop': op['stop'], - 'pc': set([op['pc']]), + 'pc': set([pc]), 'jump': False } From c79304c742fb4c144d79098f210f2b7f65449af8 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 15:42:54 +0300 Subject: [PATCH 31/72] bugfixes --- brownie/project/compiler.py | 6 +++--- brownie/project/sources.py | 4 +--- brownie/test/coverage.py | 7 ++++++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 30a8a2c35..c7757f512 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -81,7 +81,7 @@ def _compile_and_format(input_json): compiled = _generate_pcMap(compiled) result = {} - for filename, name in [(k, x) for k, v in compiled['contracts'].items() for x in v]: + for filename, name in [(k, v) for k in input_json['sources'] for v in compiled['contracts'][k]]: data = compiled['contracts'][filename][name] evm = data['evm'] ref = [ @@ -107,12 +107,12 @@ def _compile_and_format(input_json): 'deployedSourceMap': evm['deployedBytecode']['sourceMap'], # 'networks': {}, 'opcodes': evm['deployedBytecode']['opcodes'], + 'pcMap': evm['pcMap'], 'sha1': sources.get_hash(name), 'source': input_json['sources'][filename]['content'], 'sourceMap': evm['bytecode']['sourceMap'], 'sourcePath': filename, - 'type': sources.get_type(name), - 'pcMap': evm['pcMap'] + 'type': sources.get_type(name) } result[name]['coverageMap'] = _generate_coverageMap(result[name]) return result diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 8bd32c519..ad33c6864 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -14,10 +14,8 @@ def __init__(self): self._source = {} self._uncommented_source = {} self._comment_offsets = {} - self._fn_map = {} self._path = None self._data = {} - self._inheritance_map = {} self._string_iter = 1 def _load(self): @@ -115,7 +113,7 @@ def get_fn(self, name, start, stop): if start < offsets[-1][1]: return (False, -1, -1) offset = next(i for i in offsets if start >= i[1]) - return (False, -1, -1) if stop >= offset[2] else offset + return (False, -1, -1) if stop > offset[2] else offset def get_fn_offset(self, name, fn_name): if name not in self._data: diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 7f63dd72b..8597340cc 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -51,7 +51,10 @@ def analyze_coverage(history): coverage_eval[name][path][fn]['tx'].add(pcMap[name][pc]['coverageIndex']) continue # if a JUMPI, check that we hit the jump AND the related coverage map - idx = coverage_map[name][path][fn].index(next(i for i in coverage_map[name][path][fn] if i['jump']==pc)) + try: + idx = coverage_map[name][path][fn].index(next(i for i in coverage_map[name][path][fn] if i['jump']==pc)) + except StopIteration: + continue if idx not in tx_eval[name][path][fn] or idx in coverage_eval[name][path][fn]['tx']: continue key = ('false', 'true') if tx.trace[i+1]['pc'] == pc+1 else ('true', 'false') @@ -131,6 +134,7 @@ def generate_report(coverage_eval): coverage_map = build[name]['coverageMap'][path] report['highlights'][name][path] = [] for fn_name, lines in coverage_map.items(): + # if function has 0% or 100% coverage, highlight entire function if coverage[path][fn_name]['pct'] in (0, 1): color = "green" if coverage[path][fn_name]['pct'] else "red" start, stop = sources.get_fn_offset(path, fn_name) @@ -138,6 +142,7 @@ def generate_report(coverage_eval): (start, stop, color, "") ) continue + # otherwise, highlight individual statements for i, ln in enumerate(lines): if i in coverage[path][fn_name]['tx']: color = "green" From 2d74e4e619137572440e80cc00089a5b96efbe80 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 17:32:55 +0300 Subject: [PATCH 32/72] save and load coverage reports - closes #114 --- brownie/cli/gui.py | 7 +++-- brownie/cli/test.py | 62 +++++++++++++++++++++++++++------------- brownie/gui/root.py | 18 +++++++++--- brownie/test/coverage.py | 3 ++ 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/brownie/cli/gui.py b/brownie/cli/gui.py index b9b6425e0..df5912524 100644 --- a/brownie/cli/gui.py +++ b/brownie/cli/gui.py @@ -4,10 +4,11 @@ from brownie.gui import Gui -__doc__ = """Usage: brownie gui +__doc__ = """Usage: brownie gui [options] Options: - --help -h Display this message + --report -r [filename] Load and display a report + --help -h Display this message Opens the brownie GUI. Basic functionality is as follows: @@ -30,5 +31,5 @@ def main(): args = docopt(__doc__) print("Loading Brownie GUI...") - Gui().mainloop() + Gui(args['--report']).mainloop() print("GUI was terminated.") diff --git a/brownie/cli/test.py b/brownie/cli/test.py index fb42c38c0..6b756873e 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -11,7 +11,11 @@ import time from brownie.cli.utils import color -from brownie.test.coverage import merge_coverage, analyze_coverage +from brownie.test.coverage import ( + analyze_coverage, + merge_coverage, + generate_report +) from brownie.exceptions import ExpectedFailing, VirtualMachineError import brownie.network as network from brownie.network.history import TxHistory @@ -33,13 +37,14 @@ Number or range of tests to run from file Options: - - --coverage -c Evaluate test coverage and display a report - --update -u Only run tests where changes have occurred - --gas -g Display gas profile for function calls - --verbose -v Enable verbose reporting - --tb -t Show entire python traceback on exceptions - --help -h Display this message + + --update -u Only run tests where changes have occurred + --coverage -c Evaluate test coverage and display a report + --save -s [filename] Save coverage report + --gas -g Display gas profile for function calls + --verbose -v Enable verbose reporting + --tb -t Show entire python traceback on exceptions + --help -h Display this message By default brownie runs every script found in the tests folder as well as any subfolders. Files and folders beginning with an underscore will be skipped.""" @@ -60,7 +65,7 @@ def _run_test(module, fn_name, count, total): sys.stdout.write(" {0} - {1} ({0}/{2})...".format(count, desc, total)) sys.stdout.flush() args = _get_args(fn) - if args['skip'] == True or (args['skip']=="coverage" and ARGV['coverage']): + if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): sys.stdout.write( "\r {0[pending]}\u229d{0[dull]} {1} - ".format(color, count) + "{1} ({0[pending]}skipped{0[dull]}){0}\n".format(color, desc) @@ -172,6 +177,11 @@ def main(): ARGV['always_transact'] = True history = TxHistory() history._revert_lock = True + if ARGV['save']: + if not ARGV['save'].endswith('.json'): + ARGV['save'] += ".json" + if Path(ARGV['save']).exists(): + sys.exit("{0[error]}ERROR{0}: Cannot save report to {0[module]}{1}{0} - file already exists".format(color, ARGV['save'])) traceback_info = [] test_files = get_test_files(args['']) @@ -207,7 +217,6 @@ def main(): if not p.exists(): p.mkdir() - tb = run_test(filename, network, idx) if tb: traceback_info += tb @@ -258,19 +267,32 @@ def main(): print("\n{0[success]}SUCCESS{0}: All tests passed.".format(color)) if args['--coverage']: - print("\nCoverage analysis:\n") coverage_eval = merge_coverage(coverage_files) - - for contract in coverage_eval: - print(" contract: {0[contract]}{1}{0}".format(color, contract)) - for fn_name, pct in [(x,v[x]['pct']) for v in coverage_eval[contract].values() for x in v]: - c = next(i[1] for i in COVERAGE_COLORS if pct<=i[0]) - print(" {0[contract_method]}{1}{0} - {2}{3:.1%}{0}".format( - color, fn_name, color(c), pct - )) - print() + display_report(coverage_eval) + if ARGV['save']: + path = Path(ARGV['save']).resolve() + json.dump( + generate_report(coverage_eval), + path.open('w'), + sort_keys=True, + indent=2, + default=sorted + ) + print("Coverage report saved at {}".format(path)) if args['--gas']: print('\nGas Profile:') for i in sorted(transaction.gas_profile): print("{0} - avg: {1[avg]:.0f} low: {1[low]} high: {1[high]}".format(i, transaction.gas_profile[i])) + + +def display_report(coverage_eval): + print("\nCoverage analysis:\n") + for contract in coverage_eval: + print(" contract: {0[contract]}{1}{0}".format(color, contract)) + for fn_name, pct in [(x,v[x]['pct']) for v in coverage_eval[contract].values() for x in v]: + c = next(i[1] for i in COVERAGE_COLORS if pct<=i[0]) + print(" {0[contract_method]}{1}{0} - {2}{3:.1%}{0}".format( + color, fn_name, color(c), pct + )) + print() \ No newline at end of file diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 36d7943a3..5e61cc6f4 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +import json from pathlib import Path import threading @@ -22,10 +23,13 @@ class Root(tk.Tk): _active = threading.Event() - def __init__(self): + def __init__(self, report_file=None): if not CONFIG['folders']['project']: raise SystemError("No project loaded") + if report_file and not report_file.endswith('.json'): + report_file += ".json" + if self._active.is_set(): raise SystemError("GUI is already active") self._active.set() @@ -45,9 +49,15 @@ def __init__(self): self.combo = SelectContract(self, frame) self.combo.pack(side="top", expand="true", fill="x") - path = Path(CONFIG['folders']['project']).joinpath('build/coverage') - coverage_eval = merge_coverage(path.glob('**/*.json')) - self._coverage_report = generate_report(coverage_eval)['highlights'] + if report_file: + report_file = Path(report_file).resolve() + report = json.load(Path(report_file).open()) + print("Report loaded from {}".format(report_file)) + else: + path = Path(CONFIG['folders']['project']).joinpath('build/coverage') + coverage_eval = merge_coverage(path.glob('**/*.json')) + report = generate_report(coverage_eval) + self._coverage_report = report['highlights'] self._show_coverage = False self.bind("c", self._toggle_coverage) set_style(self) diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 8597340cc..d7cd69850 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -126,15 +126,18 @@ def merge_coverage(coverage_files): def generate_report(coverage_eval): report = { 'highlights':{}, + 'coverage': {}, 'sha1':{} } for name, coverage in coverage_eval.items(): report['highlights'][name] = {} + report['coverage'][name] = {} for path in coverage: coverage_map = build[name]['coverageMap'][path] report['highlights'][name][path] = [] for fn_name, lines in coverage_map.items(): # if function has 0% or 100% coverage, highlight entire function + report['coverage'][name][fn_name] = coverage[path][fn_name]['pct'] if coverage[path][fn_name]['pct'] in (0, 1): color = "green" if coverage[path][fn_name]['pct'] else "red" start, stop = sources.get_fn_offset(path, fn_name) From 6f4230c497035958ac3f6b5974c53b3dbb8c02ca Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 20:31:26 +0300 Subject: [PATCH 33/72] get_fn only return function name --- brownie/project/compiler.py | 34 ++++++++++++---------------------- brownie/project/sources.py | 16 ++++++++-------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index c7757f512..938dce29d 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -179,7 +179,7 @@ def _generate_pcMap(compiled): 'op': opcodes.pop(), 'contract': contract, 'jump': last[3], - 'fn': sources.get_fn(contract, last[0], last[0]+last[1])[0] + 'fn': sources.get_fn(contract, last[0], last[0]+last[1]) } if opcodes[-1][:2] == "0x": pcMap[pc]['value'] = opcodes.pop() @@ -188,39 +188,29 @@ def _generate_pcMap(compiled): def _generate_coverageMap(build): - """Given the compiled project as supplied by compiler.compile_contracts(), - returns the function and line based coverage maps for unit test coverage - evaluation. + """Adds coverage data to a build json. - A coverage map item is structured as follows: + A new key 'coverageMap' is created, structured as follows: { - "/path/to/contract/file.sol":{ - "functionName":{ - "fn": {}, - "line": [{}, {}, {}], - "total": int - } + "/path/to/contract/file.sol": { + "functionName": [{ + 'jump': pc of the JUMPI instruction, if it is a jump + 'start': source code start offest + 'stop': source code stop offset + }], } } - Each dict in fn/line is as follows: - - { - 'jump': pc of the JUMPI instruction, if it is a jump - 'pc': list of opcode program counters tied to the map item - 'start': source code start offset - 'stop': source code stop offset - } - """ - + Relevent items in the pcMap also have a 'coverageIndex' added that corresponds + to an entry in the coverageMap.""" line_map = _isolate_lines(build) if not line_map: return {} final = dict((i, {}) for i in set(i['contract'] for i in line_map)) for i in line_map: - fn = sources.get_fn(i['contract'], i['start'], i['stop'])[0] + fn = sources.get_fn(i['contract'], i['start'], i['stop']) if not fn: continue final[i['contract']].setdefault(fn, []).append({ diff --git a/brownie/project/sources.py b/brownie/project/sources.py index ad33c6864..d5c0b68a5 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -18,6 +18,11 @@ def __init__(self): self._data = {} self._string_iter = 1 + def __getitem__(self, key): + if key in self._data: + return self._source[self._data[key]['sourcePath']] + return self._source[str(key)] + def _load(self): base_path = Path(CONFIG['folders']['project']) self. _path = base_path.joinpath('contracts') @@ -108,12 +113,12 @@ def get_fn(self, name, start, stop): v['offset'][0] <= start <= stop <= v['offset'][1] ), False) if not name: - return (False, -1, -1) + return False offsets = self._data[name]['fn_offsets'] if start < offsets[-1][1]: - return (False, -1, -1) + return False offset = next(i for i in offsets if start >= i[1]) - return (False, -1, -1) if stop > offset[2] else offset + return False if stop > offset[2] else offset[0] def get_fn_offset(self, name, fn_name): if name not in self._data: @@ -126,11 +131,6 @@ def get_fn_offset(self, name, fn_name): def inheritance_map(self): return dict((k, v['inherited'].copy()) for k, v in self._data.items()) - def __getitem__(self, key): - if key in self._data: - return self._source[self._data[key]['sourcePath']] - return self._source[str(key)] - def add_source(self, source): path = "".format(self._string_iter) self._source[path] = source From facf5a825029b9bef4cf11e2f4b3f66a24a93e26 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 20:34:00 +0300 Subject: [PATCH 34/72] update docs --- CHANGELOG | 2 ++ brownie/test/coverage.py | 70 ++++++++++++++++++++++------------------ docs/api-project.rst | 12 ++++--- docs/api-test.rst | 7 +++- 4 files changed, 55 insertions(+), 36 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f1d8cddbc..b0385c27c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,8 @@ - Use relative paths in build json files - Revert calls-as-transactions when evaluating coverage + - Significant refactor of coverage analysis, changes to coverageMap format + - GUI highlight reports 1.0.0b4 ------- diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index d7cd69850..9bea30261 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -10,6 +10,9 @@ sources = Sources() def analyze_coverage(history): + '''Given a list of TransactionReceipt objects, analyzes test coverage and + returns a coverage evaluation dict. + ''' build_json = {} coverage_eval = {} coverage_map = {} @@ -64,10 +67,41 @@ def analyze_coverage(history): continue coverage_eval[name][path][fn][key[1]].discard(idx) coverage_eval[name][path][fn]['tx'].add(idx) - return calculate_pct(coverage_eval) + return _calculate_pct(coverage_eval) -def calculate_pct(coverage_eval): +def merge_coverage(coverage_files): + '''Given a list of coverage evaluation json file paths, returns an aggregated + coverage evaluation dict. + '''' + merged_eval = {} + for filename in coverage_files: + path = Path(filename) + if not path.exists(): + continue + coverage = json.load(path.open())['coverage'] + for contract_name in list(coverage): + if contract_name not in merged_eval: + merged_eval[contract_name] = coverage.pop(contract_name) + continue + for source, fn_name in [(k, x) for k, v in coverage[contract_name].items() for x in v]: + f = merged_eval[contract_name][source][fn_name] + c = coverage[contract_name][source][fn_name] + if not c['pct'] or f == c: + continue + if f['pct'] == 1 or c['pct'] == 1: + merged_eval[contract_name][source][fn_name] = {'pct': 1} + continue + f['true'] += c['true'] + f['false'] += c['false'] + f['tx'] = list(set(f['tx']+c['tx']+[i for i in f['true'] if i in f['false']])) + f['true'] = list(set([i for i in f['true'] if i not in f['tx']])) + f['false'] = list(set([i for i in f['false'] if i not in f['tx']])) + return _calculate_pct(merged_eval) + + +def _calculate_pct(coverage_eval): + '''Internal method to calculate coverage percentages''' for name in coverage_eval: coverage_map = build[name]['coverageMap'] for path, fn_name in [(k,x) for k,v in coverage_map.items() for x in v]: @@ -96,38 +130,12 @@ def calculate_pct(coverage_eval): return coverage_eval -def merge_coverage(coverage_files): - merged_eval = {} - for filename in coverage_files: - path = Path(filename) - if not path.exists(): - continue - coverage = json.load(path.open())['coverage'] - for contract_name in list(coverage): - if contract_name not in merged_eval: - merged_eval[contract_name] = coverage.pop(contract_name) - continue - for source, fn_name in [(k, x) for k, v in coverage[contract_name].items() for x in v]: - f = merged_eval[contract_name][source][fn_name] - c = coverage[contract_name][source][fn_name] - if not c['pct'] or f == c: - continue - if f['pct'] == 1 or c['pct'] == 1: - merged_eval[contract_name][source][fn_name] = {'pct': 1} - continue - f['true'] += c['true'] - f['false'] += c['false'] - f['tx'] = list(set(f['tx']+c['tx']+[i for i in f['true'] if i in f['false']])) - f['true'] = list(set([i for i in f['true'] if i not in f['tx']])) - f['false'] = list(set([i for i in f['false'] if i not in f['tx']])) - return calculate_pct(merged_eval) - - def generate_report(coverage_eval): + '''Converts coverage evaluation into highlight data suitable for the GUI''' report = { - 'highlights':{}, + 'highlights': {}, 'coverage': {}, - 'sha1':{} + 'sha1': {} } for name, coverage in coverage_eval.items(): report['highlights'][name] = {} diff --git a/docs/api-project.rst b/docs/api-project.rst index be91eaf15..acd478dae 100644 --- a/docs/api-project.rst +++ b/docs/api-project.rst @@ -150,10 +150,6 @@ Sources >>> from brownie.project.sources import Sources >>> s = Sources() -.. py:classmethod:: Sources.remove_comments(path) - - Given the path to a contract source file, removes comments from the source code and returns it as a string. - .. py:classmethod:: Sources.get_hash(contract_name) Returns a hash of the contract source code. This hash is generated specifically from the given contract name (not the entire containing file), after comments have been removed. @@ -166,6 +162,14 @@ Sources Returns the type of contract (contract, interface, library). +.. py:classmethod:: Sources.get_fn(name, start, stop) + + Given a contract name, start and stop offset, returns the name of the associated function. Returns ``False`` if the offset spans multiple functions. + +.. py:classmethod:: Sources.get_fn_offset(name, fn_name) + + Given a contract and function name, returns the source offsets of the function. + .. py:classmethod:: Sources.inheritance_map() Returns a set of all contracts that the given contract inherits from. diff --git a/docs/api-test.rst b/docs/api-test.rst index bf95ea673..957aeda3b 100644 --- a/docs/api-test.rst +++ b/docs/api-test.rst @@ -209,4 +209,9 @@ Module Methods .. py:method:: coverage.merge_coverage(coverage_files) - Given a list of coverage file paths, returns a single aggregated coverage report. Used by ``cli.test`` and ``gui.root`` to display coverage information. + Given a list of coverage file paths, returns a single aggregated coverage report. + +.. py:method:: coverage.generate_report(coverage_eval) + + + Given a coverage report as generated by ``analyze_coverage`` or ``merge_coverage``, returns a generic highlight report suitable for display within the Brownie GUI. From c9e36adba8e42155ac775bea150fbbf3adbe7b58 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 22:31:26 +0300 Subject: [PATCH 35/72] reformat --- brownie/cli/test.py | 244 ++++++++++++++++++++++---------------------- 1 file changed, 124 insertions(+), 120 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 6b756873e..b3f18890d 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -59,115 +59,10 @@ def _get_args(fn): )) -def _run_test(module, fn_name, count, total): - fn = getattr(module, fn_name) - desc = fn.__doc__ or fn_name - sys.stdout.write(" {0} - {1} ({0}/{2})...".format(count, desc, total)) +def _print(value, *args): + value = value.format(color, *args) + sys.stdout.write(value) sys.stdout.flush() - args = _get_args(fn) - if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): - sys.stdout.write( - "\r {0[pending]}\u229d{0[dull]} {1} - ".format(color, count) + - "{1} ({0[pending]}skipped{0[dull]}){0}\n".format(color, desc) - ) - return [] - try: - stime = time.time() - if ARGV['coverage'] and 'always_transact' in args: - ARGV['always_transact'] = args['always_transact'] - fn() - if ARGV['coverage']: - ARGV['always_transact'] = True - if args['pending']: - raise ExpectedFailing("Test was expected to fail") - sys.stdout.write("\r {0[success]}\u2713{0} {1} - {2} ({3:.4f}s) \n".format( - color, count, desc, time.time()-stime - )) - sys.stdout.flush() - return [] - except Exception as e: - if type(e) != ExpectedFailing and args['pending']: - c = [color('success'), color('dull'), color()] - else: - c = [color('error'), color('dull'), color()] - sys.stdout.write("\r {0[0]}{1}{0[1]} {4} - {2} ({0[0]}{3}{0[1]}){0[2]}\n".format( - c, - '\u2717' if type(e) in (AssertionError, VirtualMachineError) else '\u203C', - desc, - type(e).__name__, - count - )) - sys.stdout.flush() - if type(e) != ExpectedFailing and args['pending']: - return [] - filename = str(Path(module.__file__).relative_to(CONFIG['folders']['project'])) - fn_name = filename[:-2]+fn_name - return [(fn_name, color.format_tb(sys.exc_info(), filename), type(e))] - - -def run_test(filename, network, idx): - network.rpc.reset() - if type(CONFIG['test']['gas_limit']) is int: - network.gas_limit(CONFIG['test']['gas_limit']) - - module = importlib.import_module(filename.replace(os.sep, '.')) - test_names = [ - i for i in dir(module) if i not in dir(sys.modules['brownie']) and - i[0] != "_" and callable(getattr(module, i)) - ] - code = Path(CONFIG['folders']['project']).joinpath("{}.py".format(filename)).open().read() - test_names = re.findall('(?<=\ndef)[\s]{1,}[^(]*(?=\([^)]*\)[\s]*:)', code) - test_names = [i.strip() for i in test_names if i.strip()[0] != "_"] - duplicates = set([i for i in test_names if test_names.count(i) > 1]) - if duplicates: - raise ValueError( - "tests/{}.py contains multiple tests of the same name: {}".format( - filename, ", ".join(duplicates) - ) - ) - traceback_info = [] - if not test_names: - print("\n{0[error]}WARNING{0}: No test functions in {0[module]}{1}.py{0}".format(color, filename)) - return [], [] - - print("\nRunning {0[module]}{1}.py{0} - {2} test{3}".format( - color, - filename, - len(test_names)-1, - "s" if len(test_names) != 2 else "" - )) - if 'setup' in test_names: - test_names.remove('setup') - traceback_info += _run_test(module, 'setup', 0, len(test_names)) - if traceback_info: - return traceback_info - network.rpc.snapshot() - for c, t in enumerate(test_names[idx], start=idx.start + 1): - network.rpc.revert() - traceback_info += _run_test(module, t, c, len(test_names)) - if traceback_info and traceback_info[-1][2] == ReadTimeout: - print(" {0[error]}WARNING{0}: RPC crashed, terminating test".format(color)) - network.rpc.kill(False) - network.rpc.launch(CONFIG['active_network']['test-rpc']) - break - return traceback_info - - -def get_test_files(path): - if not path: - path = "" - if path[:6] != "tests/": - path = "tests/"+path - path = Path(CONFIG['folders']['project']).joinpath(path) - if not path.is_dir(): - if not path.suffix: - path = Path(str(path)+".py") - if not path.exists(): - sys.exit("{0[error]}ERROR{0}: Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) - result = [path] - else: - result = [i for i in path.glob('**/*.py') if i.name[0]!="_" and "/_" not in str(i)] - return [str(i.relative_to(CONFIG['folders']['project']))[:-3] for i in result] def main(): @@ -182,9 +77,8 @@ def main(): ARGV['save'] += ".json" if Path(ARGV['save']).exists(): sys.exit("{0[error]}ERROR{0}: Cannot save report to {0[module]}{1}{0} - file already exists".format(color, ARGV['save'])) - traceback_info = [] - test_files = get_test_files(args['']) + test_files = get_test_files(args['']) if len(test_files) == 1 and args['']: try: idx = args[''] @@ -201,6 +95,7 @@ def main(): network.connect(ARGV['network']) coverage_files = [] + traceback_info = [] try: for filename in test_files: @@ -226,14 +121,12 @@ def main(): if args['--coverage']: stime = time.time() - sys.stdout.write(" - Evaluating test coverage...") - sys.stdout.flush() + _print(" - Evaluating test coverage...") coverage_eval = analyze_coverage(history.copy()) - sys.stdout.write( - "\r {0[success]}\u2713{0} - ".format(color) + - "Evaluating test coverage ({:.4f}s)\n".format(time.time()-stime) + _print( + "\r {0[success]}\u2713{0} - Evaluating test coverage ({1:.4f}s)\n", + time.time()-stime ) - sys.stdout.flush() build_files = set(Path('build/contracts/{}.json'.format(i)) for i in coverage_eval) coverage_eval = { 'coverage': coverage_eval, @@ -244,7 +137,6 @@ def main(): test_path = Path(filename+".py") coverage_eval['sha1'][str(test_path)] = get_ast_hash(test_path) - json.dump( coverage_eval, coverage_json.open('w'), @@ -286,13 +178,125 @@ def main(): print("{0} - avg: {1[avg]:.0f} low: {1[low]} high: {1[high]}".format(i, transaction.gas_profile[i])) +def get_test_files(path): + if not path: + path = "" + if path[:6] != "tests/": + path = "tests/"+path + path = Path(CONFIG['folders']['project']).joinpath(path) + if not path.is_dir(): + if not path.suffix: + path = Path(str(path)+".py") + if not path.exists(): + sys.exit("{0[error]}ERROR{0}: Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) + result = [path] + else: + result = [i for i in path.glob('**/*.py') if i.name[0]!="_" and "/_" not in str(i)] + return [str(i.relative_to(CONFIG['folders']['project']))[:-3] for i in result] + + +def run_test(filename, network, idx): + network.rpc.reset() + if type(CONFIG['test']['gas_limit']) is int: + network.gas_limit(CONFIG['test']['gas_limit']) + + module = importlib.import_module(filename.replace(os.sep, '.')) + test_names = [ + i for i in dir(module) if i not in dir(sys.modules['brownie']) and + i[0] != "_" and callable(getattr(module, i)) + ] + code = Path(CONFIG['folders']['project']).joinpath("{}.py".format(filename)).open().read() + test_names = re.findall(r'(?<=\ndef)[\s]{1,}[^(]*(?=\([^)]*\)[\s]*:)', code) + test_names = [i.strip() for i in test_names if i.strip()[0] != "_"] + duplicates = set([i for i in test_names if test_names.count(i) > 1]) + if duplicates: + raise ValueError( + "tests/{}.py contains multiple tests of the same name: {}".format( + filename, ", ".join(duplicates) + ) + ) + traceback_info = [] + if not test_names: + print("\n{0[error]}WARNING{0}: No test functions in {0[module]}{1}.py{0}".format(color, filename)) + return [], [] + + print("\nRunning {0[module]}{1}.py{0} - {2} test{3}".format( + color, + filename, + len(test_names)-1, + "s" if len(test_names) != 2 else "" + )) + if 'setup' in test_names: + test_names.remove('setup') + traceback_info += run_test_method(module, 'setup', 0, len(test_names)) + if traceback_info: + return traceback_info + network.rpc.snapshot() + for c, t in enumerate(test_names[idx], start=idx.start + 1): + network.rpc.revert() + traceback_info += run_test_method(module, t, c, len(test_names)) + if traceback_info and traceback_info[-1][2] == ReadTimeout: + print(" {0[error]}WARNING{0}: RPC crashed, terminating test".format(color)) + network.rpc.kill(False) + network.rpc.launch(CONFIG['active_network']['test-rpc']) + break + return traceback_info + + +def run_test_method(module, fn_name, count, total): + fn = getattr(module, fn_name) + desc = fn.__doc__ or fn_name + _print(" {0} - {1} ({0}/{2})...".format(count, desc, total)) + args = _get_args(fn) + if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): + _print( + "\r {0[pending]}\u229d{0[dull]} {1} - {2} ({0[pending]}skipped{0[dull]}){0}\n", + count, + desc + ) + return [] + try: + stime = time.time() + if ARGV['coverage'] and 'always_transact' in args: + ARGV['always_transact'] = args['always_transact'] + fn() + if ARGV['coverage']: + ARGV['always_transact'] = True + if args['pending']: + raise ExpectedFailing("Test was expected to fail") + _print( + "\r {0[success]}\u2713{0} {1} - {2} ({3:.4f}s) \n", + count, + desc, + time.time()-stime + ) + return [] + except Exception as e: + if type(e) != ExpectedFailing and args['pending']: + c = [color('success'), color('dull'), color()] + else: + c = [color('error'), color('dull'), color()] + _print("\r {0[0]}{1}{0[1]} {4} - {2} ({0[0]}{3}{0[1]}){0[2]}\n".format( + c, + '\u2717' if type(e) in (AssertionError, VirtualMachineError) else '\u203C', + desc, + type(e).__name__, + count + )) + if type(e) != ExpectedFailing and args['pending']: + return [] + filename = str(Path(module.__file__).relative_to(CONFIG['folders']['project'])) + fn_name = filename[:-2]+fn_name + return [(fn_name, color.format_tb(sys.exc_info(), filename), type(e))] + + def display_report(coverage_eval): print("\nCoverage analysis:\n") for contract in coverage_eval: print(" contract: {0[contract]}{1}{0}".format(color, contract)) - for fn_name, pct in [(x,v[x]['pct']) for v in coverage_eval[contract].values() for x in v]: - c = next(i[1] for i in COVERAGE_COLORS if pct<=i[0]) + for fn_name, pct in [(x, v[x]['pct']) for v in coverage_eval[contract].values() for x in v]: + c = next(i[1] for i in COVERAGE_COLORS if pct <= i[0]) print(" {0[contract_method]}{1}{0} - {2}{3:.1%}{0}".format( color, fn_name, color(c), pct )) - print() \ No newline at end of file + print() From 66aa91836b3501d2c37b582f013c9e44a6024a23 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 10 May 2019 23:42:07 +0300 Subject: [PATCH 36/72] regex, use raw string notation --- brownie/project/sources.py | 12 ++++++------ brownie/test/coverage.py | 24 +++++++++++++----------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index d5c0b68a5..ef481a8ab 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -39,7 +39,7 @@ def _load(self): def _remove_comments(self, path): source = self._source[str(path)] offsets = [(0, 0)] - pattern = "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + pattern = r"((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" for match in re.finditer(pattern, source): offsets.append(( match.start() - offsets[-1][1], @@ -50,12 +50,12 @@ def _remove_comments(self, path): def _get_contract_data(self, path): contracts = re.findall( - "((?:contract|library|interface)[^;{]*{[\s\S]*?})\s*(?=contract|library|interface|$)", + r"((?:contract|library|interface)[^;{]*{[\s\S]*?})\s*(?=contract|library|interface|$)", self._uncommented_source[str(path)] ) for source in contracts: type_, name, inherited = re.findall( - "\s*(contract|library|interface) (\S*) (?:is (.*?)|)(?: *{)", + r"\s*(contract|library|interface) (\S*) (?:is (.*?)|)(?: *{)", source )[0] inherited = set(i.strip() for i in inherited.split(', ') if i) @@ -63,7 +63,7 @@ def _get_contract_data(self, path): self._data[name] = { 'sourcePath': str(path), 'type': type_, - 'inherited': inherited.union(re.findall("(?:;|{)\s*using *(\S*)(?= for)", source)), + 'inherited': inherited.union(re.findall(r"(?:;|{)\s*using *(\S*)(?= for)", source)), 'sha1': sha1(source.encode()).hexdigest(), 'fn_offsets': [], 'offset': ( @@ -76,9 +76,9 @@ def _get_contract_data(self, path): fn_offsets = [] for idx, pattern in enumerate(( # matches functions - "function\s*(\w*)[^{;]*{[\s\S]*?}(?=\s*function|\s*})", + r"function\s*(\w*)[^{;]*{[\s\S]*?}(?=\s*function|\s*})", # matches public variables - "(?:{|;)\s*(?!function)(\w[^;]*(?:public\s*constant|public)\s*(\w*)[^{;]*)(?=;)" + r"(?:{|;)\s*(?!function)(\w[^;]*(?:public\s*constant|public)\s*(\w*)[^{;]*)(?=;)" )): for match in re.finditer(pattern, source): fn_offsets.append(( diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 7f5c5d2fe..a65f71e4a 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -9,11 +9,11 @@ build = Build() sources = Sources() + def analyze_coverage(history): '''Given a list of TransactionReceipt objects, analyzes test coverage and returns a coverage evaluation dict. ''' - build_json = {} coverage_eval = {} coverage_map = {} pcMap = {} @@ -28,7 +28,7 @@ def analyze_coverage(history): path = t['source']['filename'] if not name or not path: continue - + # prevent repeated requests to build object if name not in pcMap: pcMap[name] = build[name]['pcMap'] @@ -36,11 +36,11 @@ def analyze_coverage(history): coverage_eval[name] = dict((i, {}) for i in coverage_map[name]) if name not in tx_eval: tx_eval[name] = dict((i, {}) for i in coverage_map[name]) - + fn = pcMap[name][pc]['fn'] if not fn: continue - + coverage_eval[name][path].setdefault(fn, {'tx': set(), 'true': set(), 'false': set()}) tx_eval[name][path].setdefault(fn, set()) if t['op'] != "JUMPI": @@ -55,7 +55,9 @@ def analyze_coverage(history): continue # if a JUMPI, check that we hit the jump AND the related coverage map try: - idx = coverage_map[name][path][fn].index(next(i for i in coverage_map[name][path][fn] if i['jump']==pc)) + idx = coverage_map[name][path][fn].index( + next(i for i in coverage_map[name][path][fn] if i['jump'] == pc) + ) except StopIteration: continue if idx not in tx_eval[name][path][fn] or idx in coverage_eval[name][path][fn]['tx']: @@ -104,7 +106,7 @@ def _calculate_pct(coverage_eval): '''Internal method to calculate coverage percentages''' for name in coverage_eval: coverage_map = build[name]['coverageMap'] - for path, fn_name in [(k,x) for k,v in coverage_map.items() for x in v]: + for path, fn_name in [(k, x) for k, v in coverage_map.items() for x in v]: result = coverage_eval[name][path] if fn_name not in result: result[fn_name] = {'pct': 0} @@ -179,7 +181,7 @@ def _evaluate_branch(path, ln): # remove comments, strip whitespace before = source[idx:start] - for pattern in ('\/\*[\s\S]*?\*\/', '\/\/[^\n]*'): + for pattern in (r'\/\*[\s\S]*?\*\/', r'\/\/[^\n]*'): for i in re.findall(pattern, before): before = before.replace(i, "") before = before.strip("\n\t (") @@ -188,10 +190,10 @@ def _evaluate_branch(path, ln): if idx <= stop: return False after = source[stop:idx].split() - after = next((i for i in after if i!=")"),after[0])[0] + after = next((i for i in after if i != ")"), after[0])[0] if ( - (before[-2:] == "if" and after=="|") or - (before[:7] == "require" and after in (")","|")) + (before[-2:] == "if" and after == "|") or + (before[:7] == "require" and after in (")", "|")) ): return True return False @@ -199,4 +201,4 @@ def _evaluate_branch(path, ln): def _maxindex(source): comp = [i for i in [";", "}", "{"] if i in source] - return max([source.rindex(i) for i in comp])+1 \ No newline at end of file + return max([source.rindex(i) for i in comp])+1 From b3f8c881ec40207ee616bfb716d92ea676863b1c Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 03:37:08 +0300 Subject: [PATCH 37/72] major refactor, add TestPrinter, use setup args as defaults --- brownie/cli/test.py | 206 ++++++++++++++++++++++++----------------- brownie/types/types.py | 8 +- 2 files changed, 128 insertions(+), 86 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index b3f18890d..450008710 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -16,7 +16,7 @@ merge_coverage, generate_report ) -from brownie.exceptions import ExpectedFailing, VirtualMachineError +from brownie.exceptions import ExpectedFailing import brownie.network as network from brownie.network.history import TxHistory import brownie.network.transaction as transaction @@ -50,21 +50,6 @@ subfolders. Files and folders beginning with an underscore will be skipped.""" -def _get_args(fn): - if not fn.__defaults__: - return FalseyDict() - return FalseyDict(zip( - fn.__code__.co_varnames[:len(fn.__defaults__)], - fn.__defaults__ - )) - - -def _print(value, *args): - value = value.format(color, *args) - sys.stdout.write(value) - sys.stdout.flush() - - def main(): args = docopt(__doc__) ARGV._update_from_args(args) @@ -112,21 +97,16 @@ def main(): if not p.exists(): p.mkdir() - tb = run_test(filename, network, idx) + tb, cov = run_test(filename, network, idx) if tb: traceback_info += tb if coverage_json.exists(): coverage_json.unlink() continue - if args['--coverage']: - stime = time.time() - _print(" - Evaluating test coverage...") - coverage_eval = analyze_coverage(history.copy()) - _print( - "\r {0[success]}\u2713{0} - Evaluating test coverage ({1:.4f}s)\n", - time.time()-stime - ) + if ARGV['coverage']: + coverage_eval = cov + build_files = set(Path('build/contracts/{}.json'.format(i)) for i in coverage_eval) coverage_eval = { 'coverage': coverage_eval, @@ -182,7 +162,7 @@ def get_test_files(path): if not path: path = "" if path[:6] != "tests/": - path = "tests/"+path + path = "tests/" + path path = Path(CONFIG['folders']['project']).joinpath(path) if not path.is_dir(): if not path.suffix: @@ -191,7 +171,7 @@ def get_test_files(path): sys.exit("{0[error]}ERROR{0}: Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) result = [path] else: - result = [i for i in path.glob('**/*.py') if i.name[0]!="_" and "/_" not in str(i)] + result = [i for i in path.glob('**/*.py') if i.name[0] != "_" and "/_" not in str(i)] return [str(i.relative_to(CONFIG['folders']['project']))[:-3] for i in result] @@ -201,62 +181,76 @@ def run_test(filename, network, idx): network.gas_limit(CONFIG['test']['gas_limit']) module = importlib.import_module(filename.replace(os.sep, '.')) - test_names = [ - i for i in dir(module) if i not in dir(sys.modules['brownie']) and - i[0] != "_" and callable(getattr(module, i)) - ] - code = Path(CONFIG['folders']['project']).joinpath("{}.py".format(filename)).open().read() - test_names = re.findall(r'(?<=\ndef)[\s]{1,}[^(]*(?=\([^)]*\)[\s]*:)', code) - test_names = [i.strip() for i in test_names if i.strip()[0] != "_"] + code = Path(filename+".py").open().read() + test_names = re.findall(r'\ndef[\s ]{1,}([^_]\w*)[\s ]*\([^)]*\)', code) duplicates = set([i for i in test_names if test_names.count(i) > 1]) if duplicates: - raise ValueError( - "tests/{}.py contains multiple tests of the same name: {}".format( - filename, ", ".join(duplicates) - ) - ) - traceback_info = [] + raise ValueError("tests/{}.py contains multiple tests of the same name: {}".format( + filename, + ", ".join(duplicates) + )) + if not test_names: print("\n{0[error]}WARNING{0}: No test functions in {0[module]}{1}.py{0}".format(color, filename)) - return [], [] + return [], {} - print("\nRunning {0[module]}{1}.py{0} - {2} test{3}".format( - color, - filename, - len(test_names)-1, - "s" if len(test_names) != 2 else "" - )) + if ARGV['coverage']: + ARGV['always_transact'] = True + always_transact = True + + traceback_info = [] if 'setup' in test_names: test_names.remove('setup') - traceback_info += run_test_method(module, 'setup', 0, len(test_names)) + fn, default_args = _get_fn(module, 'setup') + + if default_args['skip'] is True or (default_args['skip'] == "coverage" and ARGV['coverage']): + return [], {} + p = TestPrinter(filename, 0, len(test_names)) + traceback_info += run_test_method(fn, default_args, p) if traceback_info: - return traceback_info + return traceback_info, {} + else: + p = TestPrinter(filename, 1, len(test_names)) + default_args = FalseyDict() network.rpc.snapshot() - for c, t in enumerate(test_names[idx], start=idx.start + 1): + for t in test_names[idx]: network.rpc.revert() - traceback_info += run_test_method(module, t, c, len(test_names)) + fn, fn_args = _get_fn(module, t) + args = default_args.copy() + args.update(fn_args) + traceback_info += run_test_method(fn, args, p) + if ARGV['coverage']: + ARGV['always_transact'] = always_transact if traceback_info and traceback_info[-1][2] == ReadTimeout: - print(" {0[error]}WARNING{0}: RPC crashed, terminating test".format(color)) + print("{0[error]}WARNING{0}: RPC crashed, terminating test".format(color)) network.rpc.kill(False) network.rpc.launch(CONFIG['active_network']['test-rpc']) break - return traceback_info + if not traceback_info and ARGV['coverage']: + p.start("Evaluating test coverage") + coverage_eval = analyze_coverage(TxHistory().copy()) + p.stop() + return traceback_info, coverage_eval + return traceback_info, {} + + +def _get_fn(module, name): + fn = getattr(module, name) + if not fn.__defaults__: + return fn, FalseyDict() + return fn, FalseyDict(zip( + fn.__code__.co_varnames[:len(fn.__defaults__)], + fn.__defaults__ + )) -def run_test_method(module, fn_name, count, total): - fn = getattr(module, fn_name) - desc = fn.__doc__ or fn_name - _print(" {0} - {1} ({0}/{2})...".format(count, desc, total)) - args = _get_args(fn) +def run_test_method(fn, args, p): + desc = fn.__doc__ or fn.__name__ if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): - _print( - "\r {0[pending]}\u229d{0[dull]} {1} - {2} ({0[pending]}skipped{0[dull]}){0}\n", - count, - desc - ) + p.skip(desc) return [] + p.start(desc) try: - stime = time.time() if ARGV['coverage'] and 'always_transact' in args: ARGV['always_transact'] = args['always_transact'] fn() @@ -264,30 +258,20 @@ def run_test_method(module, fn_name, count, total): ARGV['always_transact'] = True if args['pending']: raise ExpectedFailing("Test was expected to fail") - _print( - "\r {0[success]}\u2713{0} {1} - {2} ({3:.4f}s) \n", - count, - desc, - time.time()-stime - ) + p.stop() return [] except Exception as e: - if type(e) != ExpectedFailing and args['pending']: - c = [color('success'), color('dull'), color()] - else: - c = [color('error'), color('dull'), color()] - _print("\r {0[0]}{1}{0[1]} {4} - {2} ({0[0]}{3}{0[1]}){0[2]}\n".format( - c, - '\u2717' if type(e) in (AssertionError, VirtualMachineError) else '\u203C', - desc, - type(e).__name__, - count - )) + p.stop(e, args['pending']) if type(e) != ExpectedFailing and args['pending']: return [] - filename = str(Path(module.__file__).relative_to(CONFIG['folders']['project'])) - fn_name = filename[:-2]+fn_name - return [(fn_name, color.format_tb(sys.exc_info(), filename), type(e))] + return [( + fn.__name__, + color.format_tb( + sys.exc_info(), + Path(sys.modules[fn.__module__].__file__).relative_to(CONFIG['folders']['project']) + ), + type(e) + )] def display_report(coverage_eval): @@ -300,3 +284,57 @@ def display_report(coverage_eval): color, fn_name, color(c), pct )) print() + + +class TestPrinter: + + def __init__(self, path, count, total): + self.path = path + self.count = count + self.total = total + print("\nRunning {0[module]}{1}.py{0} - {2} test{3}".format( + color, + path, + total, + "s" if total != 1 else "" + )) + + def skip(self, description): + self._print( + "{0} ({1[pending]}skipped{1[dull]})\n".format(description, color), + "\u229d", + "pending", + "dull" + ) + self.count += 1 + + def start(self, description): + self.desc = description + self._print("{} ({}/{})...".format(description, self.count, self.total)) + self.time = time.time() + + def stop(self, err=None, expect=False): + if not err: + self._print("{} ({:.4f}s) \n".format(self.desc, time.time() - self.time), "\u2713") + else: + err = type(err).__name__ + color_str = 'success' if expect and err != "ExpectedFailing" else 'error' + symbol = '\u2717' if err in ("AssertionError", "VirtualMachineError") else '\u203C' + msg = "{} ({}{}{})\n".format( + self.desc, + color(color_str), + err, + color('dull') + ) + self._print(msg, symbol, color_str, "dull") + self.count += 1 + + def _print(self, msg, symbol=" ", symbol_color="success", main_color=None): + sys.stdout.write("\r {}{}{} {} - {}".format( + color(symbol_color), + symbol, + color(main_color), + self.count, + msg + )) + sys.stdout.flush() diff --git a/brownie/types/types.py b/brownie/types/types.py index c5e2854c3..a0d26fdea 100644 --- a/brownie/types/types.py +++ b/brownie/types/types.py @@ -113,7 +113,10 @@ def __getitem__(self, key): def _update_from_args(self, values): '''Updates the dict from docopts.args''' - self.update(dict((k.lstrip("-"), v) for k,v in values.items())) + self.update(dict((k.lstrip("-"), v) for k, v in values.items())) + + def copy(self): + return FalseyDict(self) class EventDict: @@ -202,7 +205,8 @@ def __init__(self, name, event_data, pos): def __getitem__(self, key): '''if key is int: returns the n'th event that was fired with this name - if key is str: returns the value of data field 'key' from the 1st event within the container ''' + if key is str: returns the value of data field 'key' from the 1st event + within the container ''' if type(key) is int: return self._ordered[key] return self._ordered[0][key] From ed9f35b8e1e8b0b8b6e6cce3f1d2a80d15b8b192 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 04:03:59 +0300 Subject: [PATCH 38/72] update docs --- docs/tests.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tests.rst b/docs/tests.rst index b929abe31..682334ecc 100644 --- a/docs/tests.rst +++ b/docs/tests.rst @@ -53,6 +53,8 @@ The following keyword arguments can be used to affect how a test runs: * ``pending``: If set to ``True``, this test is expected to fail. If the test passes it will raise an ``ExpectedFailing`` exception. * ``always_transact``: If set to ``False``, calls to non state-changing methods will still execute as calls when running test coverage analysis. See :ref:`coverage` for more information. +Any arguments applied to a test module's ``setup`` method will be used as the default arguments for all that module's methods. Including ``skip=True`` on the setup method will skip the entire module. + Tests rely heavily on methods in the Brownie ``check`` module as an alternative to normal ``assert`` statements. You can read about them in the API :ref:`api_check` documentation. Example Test Script From 22dcf5e296c4348343cfe4ffc355357b907be524 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 04:33:48 +0300 Subject: [PATCH 39/72] add toolbar frame --- brownie/gui/root.py | 16 +++++++++------- brownie/gui/textbook.py | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 5e61cc6f4..cfd4777b9 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -37,17 +37,19 @@ def __init__(self, report_file=None): super().__init__(className="Opcode Viewer") self.bind("", lambda k: self.destroy()) - self.note = TextBook(self) - self.note.pack(side="left") - + # main widgets frame = ttk.Frame(self) - frame.pack(side="right", expand="true", fill="y") - + frame.pack(side="bottom", expand="true", fill="x") + self.note = TextBook(self, frame) + self.note.pack(side="left") self.tree = ListView(self, frame, (("pc", 80), ("opcode", 200)), height=30) - self.tree.pack(side="bottom") + self.tree.pack(side="right") + # toolbar widgets + frame = ttk.Frame(self) + frame.pack(side="top", expand="true", fill="both") self.combo = SelectContract(self, frame) - self.combo.pack(side="top", expand="true", fill="x") + self.combo.pack(side="right", anchor="e") if report_file: report_file = Path(report_file).resolve() diff --git a/brownie/gui/textbook.py b/brownie/gui/textbook.py index cfd744e25..2072ce6ac 100755 --- a/brownie/gui/textbook.py +++ b/brownie/gui/textbook.py @@ -12,8 +12,8 @@ class TextBook(ttk.Notebook): - def __init__(self, root): - super().__init__(root) + def __init__(self, root, frame): + super().__init__(frame) self._parent = root self._scope = None self.configure(padding=0) @@ -149,7 +149,7 @@ def __init__(self, root, text): super().__init__(root) self._text = tk.Text( self, - height = 35, + height = 33, width = 90, yscrollcommand = self._text_scroll ) @@ -158,7 +158,7 @@ def __init__(self, root, text): self._scroll.config(command=self._scrollbar_scroll) self._line_no = tk.Text( self, - height = 35, + height = 33, width = 4, yscrollcommand = self._text_scroll ) From f44ab3f831efa7c6a93e3c2235860df658dc3b0d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 04:39:25 +0300 Subject: [PATCH 40/72] linting --- brownie/gui/__init__.py | 2 +- brownie/gui/listview.py | 14 ++++++------ brownie/gui/select.py | 4 ++-- brownie/gui/styles.py | 7 +++--- brownie/gui/textbook.py | 48 +++++++++++++++++++++-------------------- 5 files changed, 38 insertions(+), 37 deletions(-) diff --git a/brownie/gui/__init__.py b/brownie/gui/__init__.py index 956b1724f..0e401d53f 100755 --- a/brownie/gui/__init__.py +++ b/brownie/gui/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/python3 -from .root import Root as Gui \ No newline at end of file +from .root import Root as Gui # noqa: F401 diff --git a/brownie/gui/listview.py b/brownie/gui/listview.py index 60789eadd..836713b67 100755 --- a/brownie/gui/listview.py +++ b/brownie/gui/listview.py @@ -26,7 +26,7 @@ def __init__(self, root, parent, columns, **kwargs): for tag, width in columns[1:]: self.heading(tag, text=tag) self.column(tag, width=width) - scroll=ttk.Scrollbar(self._frame) + scroll = ttk.Scrollbar(self._frame) scroll.pack(side="right", fill="y") self.configure(yscrollcommand=scroll.set) scroll.configure(command=self.yview) @@ -92,12 +92,12 @@ def _seek(self, event): self._seek_last = time.time() self._seek_buffer += event.char pc = sorted([int(i) for i in self._parent.pcMap])[::-1] - id_ = next(str(i) for i in pc if i<=int(self._seek_buffer)) + id_ = next(str(i) for i in pc if i <= int(self._seek_buffer)) self.selection_set(id_) def _show_all(self, event): self._parent.note.clear_scope() - for i in sorted(self._parent.pcMap, key= lambda k: int(k)): + for i in sorted(self._parent.pcMap, key=lambda k: int(k)): self.move(i, '', i) if self.selection(): self.see(self.selection()[0]) @@ -108,10 +108,10 @@ def _show_scope(self, event): pc = self._parent.pcMap[self.selection()[0]] if not pc['contract']: return - for key, value in sorted(self._parent.pcMap.items(), key= lambda k: int(k[0])): + for key, value in sorted(self._parent.pcMap.items(), key=lambda k: int(k[0])): if ( not value['contract'] or value['contract'] != pc['contract'] or - value['start'] < pc['start'] or value['stop']>pc['stop'] + value['start'] < pc['start'] or value['stop'] > pc['stop'] ): self.detach(key) else: @@ -128,7 +128,7 @@ def _highlight_opcode(self, event): else: self.tag_configure( op, - foreground="#dddd33" if op!="REVERT" else "#dd3333" + foreground="#dddd33" if op != "REVERT" else "#dd3333" ) self._highlighted.add(op) @@ -147,4 +147,4 @@ def _highlight_revert(self, event): self._highlighted.discard("REVERT") else: self.tag_configure("REVERT", foreground="#dd3333") - self._highlighted.add("REVERT") \ No newline at end of file + self._highlighted.add("REVERT") diff --git a/brownie/gui/select.py b/brownie/gui/select.py index 95d4c4d17..024ce583c 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -4,10 +4,10 @@ from tkinter import ttk from brownie.project.build import Build -from brownie._config import CONFIG build = Build() + class SelectContract(ttk.Combobox): def __init__(self, root, parent): @@ -42,4 +42,4 @@ def _select(self, event): else: tag = "NoSource" self._parent.tree.insert([str(pc), op['op']], [tag, op['op']]) - self._parent.pcMap = dict((str(k),v) for k,v in pcMap.items()) + self._parent.pcMap = dict((str(k), v) for k, v in pcMap.items()) diff --git a/brownie/gui/styles.py b/brownie/gui/styles.py index 46a0baccd..ce355a80c 100755 --- a/brownie/gui/styles.py +++ b/brownie/gui/styles.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 from tkinter import ttk -import tkinter as tk TEXT_STYLE = { 'font': ("Courier", 14), @@ -81,7 +80,7 @@ def set_style(root): background=[('active', "#272727")] ) style.layout( - 'Vertical.TScrollbar', + 'Vertical.TScrollbar', [( 'Vertical.Scrollbar.trough', { @@ -106,10 +105,10 @@ def set_style(root): style.map( "TCombobox", background=[("active", "#666666"), ("selected", "#383838")], - fieldbackground=[("readonly","#A9A9A9")] + fieldbackground=[("readonly", "#A9A9A9")] ) root.option_add("*TCombobox*Listbox*Font", (None, 18)) root.option_add("*TCombobox*Listbox.foreground", "#000000") root.option_add("*TCombobox*Listbox.background", "#A9A9A9") root.option_add("*TCombobox*Listbox.selectForeground", "#ECECEC") - root.option_add("*TCombobox*Listbox.selectBackground", "#272727") \ No newline at end of file + root.option_add("*TCombobox*Listbox.selectBackground", "#272727") diff --git a/brownie/gui/textbook.py b/brownie/gui/textbook.py index 2072ce6ac..8d6c83901 100755 --- a/brownie/gui/textbook.py +++ b/brownie/gui/textbook.py @@ -10,6 +10,7 @@ from brownie._config import CONFIG + class TextBook(ttk.Notebook): def __init__(self, root, frame): @@ -40,13 +41,13 @@ def add(self, path): def get_frame(self, label): label = Path(label).name return next(i for i in self._frames if i._label == label) - + def hide(self, label): frame = self.get_frame(label) if frame._visible: super().hide(frame) frame._visible = False - + def show(self, label): label = Path(label).name frame = next(i for i in self._frames if i._label == label) @@ -80,7 +81,7 @@ def _key(self, visible): if not visible: return f = self.active_frame() - if visible[-1] == f: + if visible[-1] == f: self.select(visible[0]) else: self.select(visible[visible.index(f)+1]) @@ -91,7 +92,7 @@ def apply_scope(self, start, stop): self._scope = [frame, start, stop] frame.tag_add('dark', 0, start) frame.tag_add('dark', stop, 'end') - for f in [v for v in self._frames if v!=frame]: + for f in [v for v in self._frames if v != frame]: f.tag_add('dark', 0, 'end') def clear_scope(self): @@ -110,7 +111,7 @@ def unmark_all(self, *tags): for f in self._frames: for tag in tags: f.tag_remove(tag) - + def _search(self, event): frame = self.active_frame() tree = self._parent.tree @@ -120,13 +121,13 @@ def _search(self, event): start, stop = frame.tag_ranges('sel') if self._scope and ( frame != self._scope[0] or - startself._scope[2] + start < self._scope[1] or + stop > self._scope[2] ): pc = False else: pc = [ - k for k,v in self._parent.pcMap.items() if + k for k, v in self._parent.pcMap.items() if v['contract'] and frame._label in v['contract'] and start >= v['start'] and stop <= v['stop'] ] @@ -134,9 +135,10 @@ def _search(self, event): frame.clear_highlight() tree.clear_selection() return + def key(k): return ( - (start - self._parent.pcMap[k]['start']) + + (start - self._parent.pcMap[k]['start']) + (self._parent.pcMap[k]['stop'] - stop) ) id_ = sorted(pc, key=key)[0] @@ -149,28 +151,28 @@ def __init__(self, root, text): super().__init__(root) self._text = tk.Text( self, - height = 33, - width = 90, - yscrollcommand = self._text_scroll + height=33, + width=90, + yscrollcommand=self._text_scroll ) self._scroll = ttk.Scrollbar(self) self._scroll.pack(side="left", fill="y") self._scroll.config(command=self._scrollbar_scroll) self._line_no = tk.Text( self, - height = 33, - width = 4, - yscrollcommand = self._text_scroll + height=33, + width=4, + yscrollcommand=self._text_scroll ) self._line_no.pack(side="left", fill="y") - self._text.pack(side="right",fill="y") + self._text.pack(side="right", fill="y") self._text.insert(1.0, text) - for k,v in TEXT_COLORS.items(): + for k, v in TEXT_COLORS.items(): self._text.tag_config(k, **v) - pattern = "((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + pattern = r"((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" for match in re.finditer(pattern, text): self.tag_add('comment', match.start(), match.end()) @@ -182,8 +184,8 @@ def __init__(self, root, text): text['state'] = "disabled" text.config(**TEXT_STYLE) text.config( - tabs = tkFont.Font(font=text['font']).measure(' '), - wrap = "none" + tabs=tkFont.Font(font=text['font']).measure(' '), + wrap="none" ) self._text.bind('', root._search) @@ -212,10 +214,10 @@ def tag_add(self, tag, start, end, see=False): def tag_ranges(self, tag): return [self._coord_to_offset(i.string) for i in self._text.tag_ranges(tag)] - + def tag_remove(self, tag): self._text.tag_remove(tag, 1.0, "end") - + def _offset_to_coord(self, value): text = self._text.get(1.0, "end") line = text[:value].count('\n') + 1 @@ -234,4 +236,4 @@ def _scrollbar_scroll(self, action, position, type=None): def _text_scroll(self, first, last, type=None): self._text.yview_moveto(first) self._line_no.yview_moveto(first) - self._scroll.set(first, last) \ No newline at end of file + self._scroll.set(first, last) From 41c47d3b19ce96cca9700ce936fbbfae2148542b Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 15:03:47 +0300 Subject: [PATCH 41/72] bugfix - scoping --- brownie/gui/select.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/brownie/gui/select.py b/brownie/gui/select.py index 024ce583c..42d2fed97 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -30,9 +30,10 @@ def _select(self, event): pcMap = deepcopy(build_json['pcMap']) self._parent.note.set_active(build_json['sourcePath']) self._parent.tree.delete_all() - for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)[1:]]: + contract = pcMap[0]['contract'] + for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)]: if ( - op['contract'] == pcMap[0]['contract'] and + op['contract'] == contract and op['start'] == pcMap[0]['start'] and op['stop'] == pcMap[0]['stop'] ): From 442cea0b30ed9f6775f451a2d88717babb29a26d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 15:03:56 +0300 Subject: [PATCH 42/72] tooltips --- brownie/gui/tooltip.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100755 brownie/gui/tooltip.py diff --git a/brownie/gui/tooltip.py b/brownie/gui/tooltip.py new file mode 100755 index 000000000..0d7b4d33d --- /dev/null +++ b/brownie/gui/tooltip.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 + +import tkinter as tk + +TOOLTIP_DELAY = 0.5 + + +class ToolTip(tk.Toplevel): + + def __init__(self, root, widget, text=None, textvariable=None): + super().__init__(root) + label = tk.Label(self, text=text, textvariable=textvariable, font=(None, 10)) + label.pack() + self.wm_overrideredirect(True) + self.withdraw() + self.kill = False + self.widget = widget + widget.bind("", self.enter) + + def enter(self, event): + self.kill = False + self.widget.bind("", self.leave) + self.after(int(TOOLTIP_DELAY*1000), self.show) + + def show(self): + if self.kill: + return + self.geometry("+{}+{}".format(self.winfo_pointerx()+5, self.winfo_pointery()+5)) + self.lift() + self.deiconify() + + def leave(self, event): + self.kill = True + self.widget.unbind("") + self.withdraw() + self.widget.bind("", self.enter) From 05d23890714d2b455426d7e28a641bbe7b539756 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 16:28:29 +0300 Subject: [PATCH 43/72] console --- brownie/gui/root.py | 19 ++++++++++++++----- brownie/gui/styles.py | 1 + brownie/gui/textbook.py | 15 ++------------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index cfd4777b9..1d45d8823 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -10,7 +10,7 @@ from .listview import ListView from .textbook import TextBook from .select import SelectContract -from .styles import set_style +from .styles import set_style, TEXT_STYLE from brownie.project.build import Build from brownie.test.coverage import merge_coverage, generate_report @@ -39,17 +39,26 @@ def __init__(self, report_file=None): # main widgets frame = ttk.Frame(self) - frame.pack(side="bottom", expand="true", fill="x") + frame.pack(side="bottom", expand=True, fill="both") + self.tree = ListView(self, frame, (("pc", 80), ("opcode", 200))) + self.tree.configure(height=30) + self.tree.pack(side="right", fill="y", expand=True) + + frame = ttk.Frame(frame) + frame.pack(side="left", fill="y", expand=True) self.note = TextBook(self, frame) - self.note.pack(side="left") - self.tree = ListView(self, frame, (("pc", 80), ("opcode", 200)), height=30) - self.tree.pack(side="right") + self.note.pack(side="top", fill="both", expand=True) + self.note.configure(width=920, height=100) + self.console = tk.Text(frame, height=1) + self.console.pack(side="bottom", fill="both") + self.console.configure(**TEXT_STYLE) # toolbar widgets frame = ttk.Frame(self) frame.pack(side="top", expand="true", fill="both") self.combo = SelectContract(self, frame) self.combo.pack(side="right", anchor="e") + self.combo.configure(width=23) if report_file: report_file = Path(report_file).resolve() diff --git a/brownie/gui/styles.py b/brownie/gui/styles.py index ce355a80c..d581829c5 100755 --- a/brownie/gui/styles.py +++ b/brownie/gui/styles.py @@ -11,6 +11,7 @@ 'inactiveselectbackground': "#4a6984", 'borderwidth': 1, 'highlightthickness': 0, + 'state': "disabled" } TEXT_COLORS = { diff --git a/brownie/gui/textbook.py b/brownie/gui/textbook.py index 8d6c83901..65fd1e11b 100755 --- a/brownie/gui/textbook.py +++ b/brownie/gui/textbook.py @@ -149,21 +149,11 @@ class TextBox(tk.Frame): def __init__(self, root, text): super().__init__(root) - self._text = tk.Text( - self, - height=33, - width=90, - yscrollcommand=self._text_scroll - ) + self._text = tk.Text(self, width=90, yscrollcommand=self._text_scroll) self._scroll = ttk.Scrollbar(self) self._scroll.pack(side="left", fill="y") self._scroll.config(command=self._scrollbar_scroll) - self._line_no = tk.Text( - self, - height=33, - width=4, - yscrollcommand=self._text_scroll - ) + self._line_no = tk.Text(self, width=4, yscrollcommand=self._text_scroll) self._line_no.pack(side="left", fill="y") self._text.pack(side="right", fill="y") @@ -181,7 +171,6 @@ def __init__(self, root, text): self._line_no.tag_add("justify", 1.0, "end") for text in (self._line_no, self._text): - text['state'] = "disabled" text.config(**TEXT_STYLE) text.config( tabs=tkFont.Font(font=text['font']).measure(' '), From 21766253fd4ade36824a1e0421e1dbd00622db69 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 19:00:00 +0300 Subject: [PATCH 44/72] refactor --- brownie/gui/listview.py | 35 +++++++++++++++------- brownie/gui/root.py | 65 +++++++++++++++++++++++++++-------------- brownie/gui/select.py | 37 +++-------------------- brownie/gui/textbook.py | 19 ++++++------ 4 files changed, 81 insertions(+), 75 deletions(-) diff --git a/brownie/gui/listview.py b/brownie/gui/listview.py index 836713b67..15a2a56c7 100755 --- a/brownie/gui/listview.py +++ b/brownie/gui/listview.py @@ -7,8 +7,7 @@ class ListView(ttk.Treeview): - def __init__(self, root, parent, columns, **kwargs): - self._parent = root + def __init__(self, parent, columns, **kwargs): self._last = "" self._seek_buffer = "" self._seek_last = 0 @@ -32,6 +31,7 @@ def __init__(self, root, parent, columns, **kwargs): scroll.configure(command=self.yview) self.tag_configure("NoSource", background="#272727") self.bind("<>", self._select_bind) + root = self.root = self._root() root.bind("a", self._show_all) root.bind("s", self._show_scope) root.bind("j", self._highlight_jumps) @@ -66,14 +66,27 @@ def selection_set(self, id_): self.focus_set() self.focus(id_) + def set_opcodes(self, pcMap): + self.delete_all() + for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)]: + if not op['contract'] or ( + op['contract'] == pcMap[0]['contract'] and + op['start'] == pcMap[0]['start'] and + op['stop'] == pcMap[0]['stop'] + ): + tag = "NoSource" + else: + tag = "{0[start]}:{0[stop]}:{0[contract]}".format(op) + self.insert([str(pc), op['op']], [tag, op['op']]) + def _select_bind(self, event): self.tag_configure(self._last, background="") try: pc = self.selection()[0] except IndexError: return - pcMap = self._parent.pcMap - note = self._parent.note + pcMap = self.root.pcMap + note = self.root.main.note tag = self.item(pc, 'tags')[0] if tag == "NoSource": note.active_frame().clear_highlight() @@ -91,13 +104,13 @@ def _seek(self, event): self._seek_buffer = "" self._seek_last = time.time() self._seek_buffer += event.char - pc = sorted([int(i) for i in self._parent.pcMap])[::-1] + pc = sorted([int(i) for i in self.root.pcMap])[::-1] id_ = next(str(i) for i in pc if i <= int(self._seek_buffer)) self.selection_set(id_) def _show_all(self, event): - self._parent.note.clear_scope() - for i in sorted(self._parent.pcMap, key=lambda k: int(k)): + self.root.main.note.clear_scope() + for i in sorted(self.root.pcMap, key=lambda k: int(k)): self.move(i, '', i) if self.selection(): self.see(self.selection()[0]) @@ -105,10 +118,10 @@ def _show_all(self, event): def _show_scope(self, event): if not self.selection(): return - pc = self._parent.pcMap[self.selection()[0]] + pc = self.root.pcMap[self.selection()[0]] if not pc['contract']: return - for key, value in sorted(self._parent.pcMap.items(), key=lambda k: int(k[0])): + for key, value in sorted(self.root.pcMap.items(), key=lambda k: int(k[0])): if ( not value['contract'] or value['contract'] != pc['contract'] or value['start'] < pc['start'] or value['stop'] > pc['stop'] @@ -117,11 +130,11 @@ def _show_scope(self, event): else: self.move(key, '', key) self.see(self.selection()[0]) - self._parent.note.apply_scope(pc['start'], pc['stop']) + self.root.main.note.apply_scope(pc['start'], pc['stop']) def _highlight_opcode(self, event): pc = self.identify_row(event.y) - op = self._parent.pcMap[pc]['op'] + op = self.root.pcMap[pc]['op'] if op in self._highlighted: self.tag_configure(op, foreground='') self._highlighted.remove(op) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 1d45d8823..bde92ad8f 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -38,27 +38,12 @@ def __init__(self, report_file=None): self.bind("", lambda k: self.destroy()) # main widgets - frame = ttk.Frame(self) - frame.pack(side="bottom", expand=True, fill="both") - self.tree = ListView(self, frame, (("pc", 80), ("opcode", 200))) - self.tree.configure(height=30) - self.tree.pack(side="right", fill="y", expand=True) - - frame = ttk.Frame(frame) - frame.pack(side="left", fill="y", expand=True) - self.note = TextBook(self, frame) - self.note.pack(side="top", fill="both", expand=True) - self.note.configure(width=920, height=100) - self.console = tk.Text(frame, height=1) - self.console.pack(side="bottom", fill="both") - self.console.configure(**TEXT_STYLE) + self.main = MainFrame(self) + self.main.pack(side="bottom", expand=True, fill="both") # toolbar widgets - frame = ttk.Frame(self) - frame.pack(side="top", expand="true", fill="both") - self.combo = SelectContract(self, frame) - self.combo.pack(side="right", anchor="e") - self.combo.configure(width=23) + self.toolbar = ToolbarFrame(self) + self.toolbar.pack(side="top", expand="true", fill="both") if report_file: report_file = Path(report_file).resolve() @@ -74,19 +59,55 @@ def __init__(self, report_file=None): set_style(self) def _toggle_coverage(self, event): - active = self.combo.get() + active = self.toolbar.combo.get() if not active or active not in self._coverage_report: return if self._show_coverage: - self.note.unmark_all('green', 'red', 'yellow', 'orange') + self.main.note.unmark_all('green', 'red', 'yellow', 'orange') self._show_coverage = False return for path, item in [(k, x) for k, v in self._coverage_report[active].items() for x in v]: label = Path(path).name - self.note.mark(label, item[2], item[0], item[1]) + self.main.note.mark(label, item[2], item[0], item[1]) self._show_coverage = True def destroy(self): super().destroy() self.quit() self._active.clear() + + def set_active(self, contract_name): + build_json = build[contract_name] + self.main.note.set_visible(build_json['allSourcePaths']) + self.main.note.set_active(build_json['sourcePath']) + self.main.oplist.set_opcodes(build_json['pcMap']) + self.pcMap = dict((str(k), v) for k, v in build_json['pcMap'].items()) + + +class MainFrame(ttk.Frame): + + def __init__(self, root): + super().__init__(root) + self.oplist = ListView(self, (("pc", 80), ("opcode", 200))) + self.oplist.configure(height=30) + self.oplist.pack(side="right", fill="y", expand=True) + + frame = ttk.Frame(self) + frame.pack(side="left", fill="y", expand=True) + self.note = TextBook(frame) + self.note.pack(side="top", fill="both", expand=True) + self.note.configure(width=920, height=100) + self.console = tk.Text(frame, height=1) + self.console.pack(side="bottom", fill="both") + self.console.configure(**TEXT_STYLE) + + +class ToolbarFrame(ttk.Frame): + + def __init__(self, root): + super().__init__(root) + + # contract selection + self.combo = SelectContract(self, [k for k, v in build.items() if v['bytecode']]) + self.combo.pack(side="right", anchor="e") + self.combo.configure(width=23) diff --git a/brownie/gui/select.py b/brownie/gui/select.py index 42d2fed97..715172d4e 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -1,46 +1,17 @@ #!/usr/bin/python3 -from copy import deepcopy from tkinter import ttk -from brownie.project.build import Build - -build = Build() - class SelectContract(ttk.Combobox): - def __init__(self, root, parent): - self._parent = root + def __init__(self, parent, values): super().__init__(parent, state='readonly', font=(None, 16)) - values = [] - for name, data in build.items(): - if data['bytecode']: - values.append(name) + self.root = self._root() self['values'] = sorted(values) - root.note.set_visible([]) self.bind("<>", self._select) def _select(self, event): - self._parent.note.set_visible([]) - build_json = build[self.get()] + value = self.get() self.selection_clear() - for contract in build_json['allSourcePaths']: - self._parent.note.show(contract) - pcMap = deepcopy(build_json['pcMap']) - self._parent.note.set_active(build_json['sourcePath']) - self._parent.tree.delete_all() - contract = pcMap[0]['contract'] - for pc, op in [(i, pcMap[i]) for i in sorted(pcMap)]: - if ( - op['contract'] == contract and - op['start'] == pcMap[0]['start'] and - op['stop'] == pcMap[0]['stop'] - ): - op['contract'] = None - if op['contract']: - tag = "{0[start]}:{0[stop]}:{0[contract]}".format(op) - else: - tag = "NoSource" - self._parent.tree.insert([str(pc), op['op']], [tag, op['op']]) - self._parent.pcMap = dict((str(k), v) for k, v in pcMap.items()) + self.root.set_active(value) diff --git a/brownie/gui/textbook.py b/brownie/gui/textbook.py index 65fd1e11b..8b9d8ddd3 100755 --- a/brownie/gui/textbook.py +++ b/brownie/gui/textbook.py @@ -13,17 +13,18 @@ class TextBook(ttk.Notebook): - def __init__(self, root, frame): - super().__init__(frame) - self._parent = root + def __init__(self, parent): + super().__init__(parent) + self.root = self._root() self._scope = None self.configure(padding=0) self._frames = [] - root.bind("", self.key_left) - root.bind("", self.key_right) + self.root.bind("", self.key_left) + self.root.bind("", self.key_right) base_path = Path(CONFIG['folders']['project']).joinpath('contracts') for path in base_path.glob('**/*.sol'): self.add(path) + self.set_visible([]) def add(self, path): path = Path(path) @@ -114,7 +115,7 @@ def unmark_all(self, *tags): def _search(self, event): frame = self.active_frame() - tree = self._parent.tree + tree = self.root.main.oplist if not frame.tag_ranges('sel'): tree.clear_selection() return @@ -127,7 +128,7 @@ def _search(self, event): pc = False else: pc = [ - k for k, v in self._parent.pcMap.items() if + k for k, v in self.root.pcMap.items() if v['contract'] and frame._label in v['contract'] and start >= v['start'] and stop <= v['stop'] ] @@ -138,8 +139,8 @@ def _search(self, event): def key(k): return ( - (start - self._parent.pcMap[k]['start']) + - (self._parent.pcMap[k]['stop'] - stop) + (start - self.root.pcMap[k]['start']) + + (self.root.pcMap[k]['stop'] - stop) ) id_ = sorted(pc, key=key)[0] tree.selection_set(id_) From f4dbccd5695df26b6ea0311168b3cf8b0e643f51 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 20:26:05 +0300 Subject: [PATCH 45/72] add toggle buttons, more refactoring --- brownie/gui/buttons.py | 77 +++++++++++++++++++++++++++++++++++++++++ brownie/gui/listview.py | 26 -------------- brownie/gui/root.py | 8 +++++ brownie/gui/select.py | 1 + 4 files changed, 86 insertions(+), 26 deletions(-) create mode 100755 brownie/gui/buttons.py diff --git a/brownie/gui/buttons.py b/brownie/gui/buttons.py new file mode 100755 index 000000000..2f9155922 --- /dev/null +++ b/brownie/gui/buttons.py @@ -0,0 +1,77 @@ +#!/usr/bin/python3 + +import tkinter as tk + + +class _Toggle(tk.Button): + + def __init__(self, parent, text, keybind=None): + self._active = False + super().__init__(parent, text=text, command=self.toggle) + self.root = self._root() + if keybind: + self.root.bind(keybind, self.toggle) + + def toggle(self, event=None): + if self._active: + self.toggle_off() + self.configure(relief="raised") + else: + if not self.toggle_on(): + return + self.configure(relief="sunken") + self._active = not self._active + + def toggle_on(self): + pass + + def toggle_off(self): + pass + + +class ScopingToggle(_Toggle): + + def __init__(self, parent): + super().__init__(parent, "Scope", "s") + self.oplist = self.root.main.oplist + + def toggle_on(self): + try: + op = self.oplist.selection()[0] + except IndexError: + return False + if self.oplist.item(op, 'tags')[0] == "NoSource": + return False + pc = self.root.pcMap[op] + for key, value in sorted(self.root.pcMap.items(), key=lambda k: int(k[0])): + if ( + not value['contract'] or value['contract'] != pc['contract'] or + value['start'] < pc['start'] or value['stop'] > pc['stop'] + ): + self.oplist.detach(key) + else: + self.oplist.move(key, '', key) + self.oplist.see(op) + self.root.main.note.apply_scope(pc['start'], pc['stop']) + return True + + def toggle_off(self): + self.root.main.note.clear_scope() + for i in sorted(self.root.pcMap, key=lambda k: int(k)): + self.oplist.move(i, '', i) + if self.oplist.selection(): + self.oplist.see(self.oplist.selection()[0]) + + +class ConsoleToggle(_Toggle): + + def __init__(self, parent): + super().__init__(parent, "Console", "c") + self.console = self.root.main.console + + def toggle_on(self): + self.console.config(height=10) + return True + + def toggle_off(self): + self.console.config(height=1) diff --git a/brownie/gui/listview.py b/brownie/gui/listview.py index 15a2a56c7..6ee907024 100755 --- a/brownie/gui/listview.py +++ b/brownie/gui/listview.py @@ -32,8 +32,6 @@ def __init__(self, parent, columns, **kwargs): self.tag_configure("NoSource", background="#272727") self.bind("<>", self._select_bind) root = self.root = self._root() - root.bind("a", self._show_all) - root.bind("s", self._show_scope) root.bind("j", self._highlight_jumps) root.bind("r", self._highlight_revert) self.bind("<3>", self._highlight_opcode) @@ -108,30 +106,6 @@ def _seek(self, event): id_ = next(str(i) for i in pc if i <= int(self._seek_buffer)) self.selection_set(id_) - def _show_all(self, event): - self.root.main.note.clear_scope() - for i in sorted(self.root.pcMap, key=lambda k: int(k)): - self.move(i, '', i) - if self.selection(): - self.see(self.selection()[0]) - - def _show_scope(self, event): - if not self.selection(): - return - pc = self.root.pcMap[self.selection()[0]] - if not pc['contract']: - return - for key, value in sorted(self.root.pcMap.items(), key=lambda k: int(k[0])): - if ( - not value['contract'] or value['contract'] != pc['contract'] or - value['start'] < pc['start'] or value['stop'] > pc['stop'] - ): - self.detach(key) - else: - self.move(key, '', key) - self.see(self.selection()[0]) - self.root.main.note.apply_scope(pc['start'], pc['stop']) - def _highlight_opcode(self, event): pc = self.identify_row(event.y) op = self.root.pcMap[pc]['op'] diff --git a/brownie/gui/root.py b/brownie/gui/root.py index bde92ad8f..fc3559a27 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -7,6 +7,7 @@ import tkinter as tk from tkinter import ttk +from .buttons import ScopingToggle, ConsoleToggle from .listview import ListView from .textbook import TextBook from .select import SelectContract @@ -106,8 +107,15 @@ class ToolbarFrame(ttk.Frame): def __init__(self, root): super().__init__(root) + self.root = root # contract selection self.combo = SelectContract(self, [k for k, v in build.items() if v['bytecode']]) self.combo.pack(side="right", anchor="e") self.combo.configure(width=23) + + button = ScopingToggle(self) + button.pack(side="left") + + button = ConsoleToggle(self) + button.pack(side="left") diff --git a/brownie/gui/select.py b/brownie/gui/select.py index 715172d4e..bf52fdc52 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -9,6 +9,7 @@ def __init__(self, parent, values): super().__init__(parent, state='readonly', font=(None, 16)) self.root = self._root() self['values'] = sorted(values) + self.set("Select a Contract") self.bind("<>", self._select) def _select(self, event): From 1ad183bd842070f73042df112c60dd91bf0fddef Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 20:59:19 +0300 Subject: [PATCH 46/72] show reports --- brownie/gui/buttons.py | 25 +++++++++++++++++ brownie/gui/root.py | 63 +++++++++++++++++------------------------- brownie/gui/select.py | 39 +++++++++++++++++++++++--- 3 files changed, 85 insertions(+), 42 deletions(-) diff --git a/brownie/gui/buttons.py b/brownie/gui/buttons.py index 2f9155922..43645ca46 100755 --- a/brownie/gui/buttons.py +++ b/brownie/gui/buttons.py @@ -1,5 +1,6 @@ #!/usr/bin/python3 +from pathlib import Path import tkinter as tk @@ -75,3 +76,27 @@ def toggle_on(self): def toggle_off(self): self.console.config(height=1) + + +class HighlightsToggle(_Toggle): + + def __init__(self, parent): + super().__init__(parent, "Report", "r") + self.note = self.root.main.note + + def toggle_on(self): + if not self.root.active_report: + return False + report = self.root.active_report['highlights'][self.root.get_active()] + for path, item in [(k, x) for k, v in report.items() for x in v]: + label = Path(path).name + self.note.mark(label, item[2], item[0], item[1]) + return True + + def toggle_off(self): + self.note.unmark_all('green', 'red', 'yellow', 'orange') + + def reset(self): + self.toggle_off() + self._active = False + self.toggle() diff --git a/brownie/gui/root.py b/brownie/gui/root.py index fc3559a27..4ec17407a 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -1,20 +1,18 @@ #!/usr/bin/python3 -import json from pathlib import Path import threading import tkinter as tk from tkinter import ttk -from .buttons import ScopingToggle, ConsoleToggle +from .buttons import ScopingToggle, ConsoleToggle, HighlightsToggle from .listview import ListView from .textbook import TextBook -from .select import SelectContract +from .select import ContractSelect, ReportSelect from .styles import set_style, TEXT_STYLE from brownie.project.build import Build -from brownie.test.coverage import merge_coverage, generate_report from brownie._config import CONFIG build = Build() @@ -46,37 +44,9 @@ def __init__(self, report_file=None): self.toolbar = ToolbarFrame(self) self.toolbar.pack(side="top", expand="true", fill="both") - if report_file: - report_file = Path(report_file).resolve() - report = json.load(Path(report_file).open()) - print("Report loaded from {}".format(report_file)) - else: - path = Path(CONFIG['folders']['project']).joinpath('build/coverage') - coverage_eval = merge_coverage(path.glob('**/*.json')) - report = generate_report(coverage_eval) - self._coverage_report = report['highlights'] - self._show_coverage = False - self.bind("c", self._toggle_coverage) + self.active_report = False set_style(self) - def _toggle_coverage(self, event): - active = self.toolbar.combo.get() - if not active or active not in self._coverage_report: - return - if self._show_coverage: - self.main.note.unmark_all('green', 'red', 'yellow', 'orange') - self._show_coverage = False - return - for path, item in [(k, x) for k, v in self._coverage_report[active].items() for x in v]: - label = Path(path).name - self.main.note.mark(label, item[2], item[0], item[1]) - self._show_coverage = True - - def destroy(self): - super().destroy() - self.quit() - self._active.clear() - def set_active(self, contract_name): build_json = build[contract_name] self.main.note.set_visible(build_json['allSourcePaths']) @@ -84,6 +54,14 @@ def set_active(self, contract_name): self.main.oplist.set_opcodes(build_json['pcMap']) self.pcMap = dict((str(k), v) for k, v in build_json['pcMap'].items()) + def get_active(self): + return self.toolbar.combo.get() + + def destroy(self): + super().destroy() + self.quit() + self._active.clear() + class MainFrame(ttk.Frame): @@ -110,12 +88,21 @@ def __init__(self, root): self.root = root # contract selection - self.combo = SelectContract(self, [k for k, v in build.items() if v['bytecode']]) + self.combo = ContractSelect(self, [k for k, v in build.items() if v['bytecode']]) self.combo.pack(side="right", anchor="e") self.combo.configure(width=23) - button = ScopingToggle(self) - button.pack(side="left") + path = Path(CONFIG['folders']['project']).joinpath('reports') + + self.report = ReportSelect(self, list(path.glob('**/*.json'))) + self.report.pack(side="right", anchor="e") + self.report.configure(width=23) + + self.scope = ScopingToggle(self) + self.scope.pack(side="left") + + self.console = ConsoleToggle(self) + self.console.pack(side="left") - button = ConsoleToggle(self) - button.pack(side="left") + self.highlight = HighlightsToggle(self) + self.highlight.pack(side="left") diff --git a/brownie/gui/select.py b/brownie/gui/select.py index bf52fdc52..fcdfaf3c8 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -1,18 +1,49 @@ #!/usr/bin/python3 +import json from tkinter import ttk -class SelectContract(ttk.Combobox): +class _Select(ttk.Combobox): - def __init__(self, parent, values): + def __init__(self, parent, initial, values): super().__init__(parent, state='readonly', font=(None, 16)) self.root = self._root() self['values'] = sorted(values) - self.set("Select a Contract") + self.set(initial) self.bind("<>", self._select) - def _select(self, event): + def _select(self): value = self.get() self.selection_clear() + return value + + +class ContractSelect(_Select): + + def __init__(self, parent, values): + super().__init__(parent, "Select a Contract", values) + + def _select(self, event): + value = super()._select() self.root.set_active(value) + + +class ReportSelect(_Select): + + def __init__(self, parent, report_paths): + self._reports = {} + + for path in report_paths: + try: + self._reports[path.stem] = json.load(path.open()) + except Exception: + continue + super().__init__(parent, "Reports", sorted(self._reports)) + + def _select(self, event): + value = super()._select() + if self.root.active_report == self._reports[value]: + return + self.root.active_report = self._reports[value] + self.root.toolbar.highlight.reset() From bbc7ceb4f92cdffa76c41eaa7c512437d493a331 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 21:12:00 +0300 Subject: [PATCH 47/72] rename reports to highlights, check for reports and active contract --- brownie/gui/buttons.py | 7 +++++-- brownie/gui/select.py | 9 +++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/brownie/gui/buttons.py b/brownie/gui/buttons.py index 43645ca46..e81b8dcf1 100755 --- a/brownie/gui/buttons.py +++ b/brownie/gui/buttons.py @@ -81,13 +81,16 @@ def toggle_off(self): class HighlightsToggle(_Toggle): def __init__(self, parent): - super().__init__(parent, "Report", "r") + super().__init__(parent, "Highlights", "h") self.note = self.root.main.note def toggle_on(self): if not self.root.active_report: return False - report = self.root.active_report['highlights'][self.root.get_active()] + contract = self.root.get_active() + if contract not in self.root.active_report['highlights']: + return False + report = self.root.active_report['highlights'][contract] for path, item in [(k, x) for k, v in report.items() for x in v]: label = Path(path).name self.note.mark(label, item[2], item[0], item[1]) diff --git a/brownie/gui/select.py b/brownie/gui/select.py index fcdfaf3c8..01eaa973f 100755 --- a/brownie/gui/select.py +++ b/brownie/gui/select.py @@ -33,13 +33,18 @@ class ReportSelect(_Select): def __init__(self, parent, report_paths): self._reports = {} - for path in report_paths: try: self._reports[path.stem] = json.load(path.open()) except Exception: continue - super().__init__(parent, "Reports", sorted(self._reports)) + super().__init__( + parent, + "Select a Report" if self._reports else "No Available Reports", + sorted(self._reports) + ) + if not self._reports: + self.config(state="disabled") def _select(self, event): value = super()._select() From 00d0ca39d7a02c133af17ed6309b1765eb4b48c3 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 21:12:39 +0300 Subject: [PATCH 48/72] remove report_file arg for Gui --- brownie/cli/gui.py | 7 +++---- brownie/gui/root.py | 5 +---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/brownie/cli/gui.py b/brownie/cli/gui.py index df5912524..ec93dcac5 100644 --- a/brownie/cli/gui.py +++ b/brownie/cli/gui.py @@ -4,10 +4,9 @@ from brownie.gui import Gui -__doc__ = """Usage: brownie gui [options] +__doc__ = """Usage: brownie gui Options: - --report -r [filename] Load and display a report --help -h Display this message Opens the brownie GUI. Basic functionality is as follows: @@ -29,7 +28,7 @@ def main(): - args = docopt(__doc__) + docopt(__doc__) print("Loading Brownie GUI...") - Gui(args['--report']).mainloop() + Gui().mainloop() print("GUI was terminated.") diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 4ec17407a..149edf3ea 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -22,13 +22,10 @@ class Root(tk.Tk): _active = threading.Event() - def __init__(self, report_file=None): + def __init__(self): if not CONFIG['folders']['project']: raise SystemError("No project loaded") - if report_file and not report_file.endswith('.json'): - report_file += ".json" - if self._active.is_set(): raise SystemError("GUI is already active") self._active.set() From 23931103c911166543a1e902333aaf38ae934781 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 21:28:50 +0300 Subject: [PATCH 49/72] create reports/ folder, always save reports --- brownie/cli/init.py | 1 + brownie/cli/test.py | 30 ++++++++++++------------------ brownie/project/__init__.py | 4 +++- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/brownie/cli/init.py b/brownie/cli/init.py index 3e608eb7c..24fba7a9f 100644 --- a/brownie/cli/init.py +++ b/brownie/cli/init.py @@ -24,6 +24,7 @@ contracts/ Contract source code scripts/ Scripts for deployment and interaction tests/ Scripts for project testing +reports/ Report files for contract analysis brownie-config.json Project configuration file""" diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 450008710..44cd2b2f9 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -39,8 +39,7 @@ Options: --update -u Only run tests where changes have occurred - --coverage -c Evaluate test coverage and display a report - --save -s [filename] Save coverage report + --coverage -c Evaluate test coverage --gas -g Display gas profile for function calls --verbose -v Enable verbose reporting --tb -t Show entire python traceback on exceptions @@ -57,11 +56,6 @@ def main(): ARGV['always_transact'] = True history = TxHistory() history._revert_lock = True - if ARGV['save']: - if not ARGV['save'].endswith('.json'): - ARGV['save'] += ".json" - if Path(ARGV['save']).exists(): - sys.exit("{0[error]}ERROR{0}: Cannot save report to {0[module]}{1}{0} - file already exists".format(color, ARGV['save'])) test_files = get_test_files(args['']) if len(test_files) == 1 and args['']: @@ -71,7 +65,7 @@ def main(): idx = slice(*[int(i)-1 for i in idx.split(':')]) else: idx = slice(int(idx)-1, int(idx)) - except: + except Exception: sys.exit("{0[error]}ERROR{0}: Invalid range. Must be an integer or slice (eg. 1:4)".format(color)) elif args['']: sys.exit("{0[error]}ERROR:{0} Cannot specify a range when running multiple tests files.".format(color)) @@ -141,16 +135,16 @@ def main(): if args['--coverage']: coverage_eval = merge_coverage(coverage_files) display_report(coverage_eval) - if ARGV['save']: - path = Path(ARGV['save']).resolve() - json.dump( - generate_report(coverage_eval), - path.open('w'), - sort_keys=True, - indent=2, - default=sorted - ) - print("Coverage report saved at {}".format(path)) + filename = "reports/coverage-{}.json".format(time.strftime('%d%m%y')) + path = Path(CONFIG['folders']['project']).joinpath(filename) + json.dump( + generate_report(coverage_eval), + path.open('w'), + sort_keys=True, + indent=2, + default=sorted + ) + print("Coverage report saved at {}".format(path.relative_to(CONFIG['folders']['project']))) if args['--gas']: print('\nGas Profile:') diff --git a/brownie/project/__init__.py b/brownie/project/__init__.py index a8c1ee89d..05ca9cb39 100644 --- a/brownie/project/__init__.py +++ b/brownie/project/__init__.py @@ -17,7 +17,7 @@ project = sys.modules[__name__] -FOLDERS = ["contracts", "scripts", "tests"] +FOLDERS = ["contracts", "scripts", "reports", "tests"] def check_for_project(path): @@ -76,6 +76,8 @@ def load(path=None): raise SystemError("Could not find brownie project") path = Path(path).resolve() CONFIG['folders']['project'] = str(path) + for folder in [i for i in FOLDERS]: + path.joinpath(folder).mkdir(exist_ok=True) sys.path.insert(0, str(path)) load_project_config() compiler.set_solc_version() From 8df33767ad182bd30d8326da1716e14242df9dae Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 11 May 2019 21:58:23 +0300 Subject: [PATCH 50/72] include contract name with function name, support fallback function --- brownie/project/sources.py | 2 +- brownie/test/coverage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index ef481a8ab..63d1ec9a7 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -82,7 +82,7 @@ def _get_contract_data(self, path): )): for match in re.finditer(pattern, source): fn_offsets.append(( - match.groups()[idx], + name+"."+(match.groups()[idx] or ""), self._commented_offset(path, match.start(idx) + offset), self._commented_offset(path, match.end(idx) + offset) )) diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index a65f71e4a..e4a167917 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -176,7 +176,7 @@ def _evaluate_branch(path, ln): start, stop = ln['start'], ln['stop'] try: idx = _maxindex(source[:start]) - except: + except Exception: return False # remove comments, strip whitespace From 24942378fa83c16db3c02d16ec9587e91745c951 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 01:08:20 +0300 Subject: [PATCH 51/72] reset highlight when switching contracts --- brownie/gui/buttons.py | 9 +++++---- brownie/gui/root.py | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/brownie/gui/buttons.py b/brownie/gui/buttons.py index e81b8dcf1..d63e16e3b 100755 --- a/brownie/gui/buttons.py +++ b/brownie/gui/buttons.py @@ -7,21 +7,21 @@ class _Toggle(tk.Button): def __init__(self, parent, text, keybind=None): - self._active = False + self.active = False super().__init__(parent, text=text, command=self.toggle) self.root = self._root() if keybind: self.root.bind(keybind, self.toggle) def toggle(self, event=None): - if self._active: + if self.active: self.toggle_off() self.configure(relief="raised") else: if not self.toggle_on(): return self.configure(relief="sunken") - self._active = not self._active + self.active = not self.active def toggle_on(self): pass @@ -101,5 +101,6 @@ def toggle_off(self): def reset(self): self.toggle_off() - self._active = False + self.configure(relief="raised") + self.active = False self.toggle() diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 149edf3ea..664b05ebc 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -50,6 +50,8 @@ def set_active(self, contract_name): self.main.note.set_active(build_json['sourcePath']) self.main.oplist.set_opcodes(build_json['pcMap']) self.pcMap = dict((str(k), v) for k, v in build_json['pcMap'].items()) + if self.toolbar.highlight.active: + self.toolbar.highlight.reset() def get_active(self): return self.toolbar.combo.get() From 58c216a3eae85a9ae8547bfd09558c8556d2a5a2 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 02:44:02 +0300 Subject: [PATCH 52/72] styling (my favorite activity with tkinter!) --- brownie/gui/buttons.py | 9 ++++++--- brownie/gui/listview.py | 2 +- brownie/gui/root.py | 5 +++-- brownie/gui/styles.py | 41 ++++++++++++++++++++++++++++------------- brownie/gui/textbook.py | 1 + 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/brownie/gui/buttons.py b/brownie/gui/buttons.py index d63e16e3b..642736d7a 100755 --- a/brownie/gui/buttons.py +++ b/brownie/gui/buttons.py @@ -3,6 +3,8 @@ from pathlib import Path import tkinter as tk +from .styles import BUTTON_STYLE + class _Toggle(tk.Button): @@ -10,17 +12,18 @@ def __init__(self, parent, text, keybind=None): self.active = False super().__init__(parent, text=text, command=self.toggle) self.root = self._root() + self.configure(**BUTTON_STYLE) if keybind: self.root.bind(keybind, self.toggle) def toggle(self, event=None): if self.active: self.toggle_off() - self.configure(relief="raised") + self.configure(relief="raised", background="#272727") else: if not self.toggle_on(): return - self.configure(relief="sunken") + self.configure(relief="sunken", background="#383838") self.active = not self.active def toggle_on(self): @@ -101,6 +104,6 @@ def toggle_off(self): def reset(self): self.toggle_off() - self.configure(relief="raised") + self.configure(relief="raised", background="#272727") self.active = False self.toggle() diff --git a/brownie/gui/listview.py b/brownie/gui/listview.py index 6ee907024..f04ea25e2 100755 --- a/brownie/gui/listview.py +++ b/brownie/gui/listview.py @@ -29,7 +29,7 @@ def __init__(self, parent, columns, **kwargs): scroll.pack(side="right", fill="y") self.configure(yscrollcommand=scroll.set) scroll.configure(command=self.yview) - self.tag_configure("NoSource", background="#272727") + self.tag_configure("NoSource", background="#161616") self.bind("<>", self._select_bind) root = self.root = self._root() root.bind("j", self._highlight_jumps) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 664b05ebc..642ddb265 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -30,7 +30,8 @@ def __init__(self): raise SystemError("GUI is already active") self._active.set() - super().__init__(className="Opcode Viewer") + name = Path(CONFIG['folders']['project']).name + super().__init__(className=" Brownie GUI - "+name) self.bind("", lambda k: self.destroy()) # main widgets @@ -94,7 +95,7 @@ def __init__(self, root): path = Path(CONFIG['folders']['project']).joinpath('reports') self.report = ReportSelect(self, list(path.glob('**/*.json'))) - self.report.pack(side="right", anchor="e") + self.report.pack(side="right", anchor="e", padx=10) self.report.configure(width=23) self.scope = ScopingToggle(self) diff --git a/brownie/gui/styles.py b/brownie/gui/styles.py index d581829c5..6e591643c 100755 --- a/brownie/gui/styles.py +++ b/brownie/gui/styles.py @@ -5,11 +5,11 @@ TEXT_STYLE = { 'font': ("Courier", 14), 'background': "#383838", - 'foreground': "#ECECEC", + 'foreground': "#FFFFFF", 'selectforeground': "white", 'selectbackground': "#4a6984", 'inactiveselectbackground': "#4a6984", - 'borderwidth': 1, + 'borderwidth': 0, 'highlightthickness': 0, 'state': "disabled" } @@ -24,42 +24,56 @@ } +BUTTON_STYLE = { + 'borderwidth': 1, + 'background': "#272727", + 'foreground': "#ECECEC", + 'highlightthickness': 0, + 'activebackground': "#383838", + 'activeforeground': "white" +} + + def set_style(root): style = ttk.Style() style.theme_use('default') style.configure( "Treeview", - background="#383838", + background="#272727", fieldbackground="#383838", foreground="#ECECEC", font=(None, 16), rowheight=21, - borderwidth=1 + borderwidth=0, + relief="flat", ) style.configure( "Treeview.Heading", background="#161616", foreground="#ECECEC", - borderwidth=0, - font=(None, 16) + borderwidth=2, + font=(None, 16), + relief="flat", ) style.map( "Treeview.Heading", background=[("active", "#383838"), ("selected", "#383838")], foreground=[("active", "#ECECEC"), ("selected", "#ECECEC")] ) - style.configure("TNotebook", background="#272727") + style.configure("TNotebook", background="#161616", borderwidth=0) style.configure( "TNotebook.Tab", background="#272727", foreground="#a9a9a9", - font=(None, 14) + font=(None, 14), + borderwidth=1, + relief="flat", ) style.map( "TNotebook.Tab", background=[("active", "#383838"), ("selected", "#383838")], - foreground=[("active", "#ECECEC"), ("selected", "#ECECEC")] + foreground=[("active", "#ECECEC"), ("selected", "#ECECEC")], ) style.configure( "TFrame", @@ -74,11 +88,11 @@ def set_style(root): arrowsize=16, relief="flat", borderwidth=0, - arrowcolor="#a9a9a9" + arrowcolor="#a9a9a9", ) style.map( "TScrollbar", - background=[('active', "#272727")] + background=[('active', "#272727")], ) style.layout( 'Vertical.TScrollbar', @@ -100,13 +114,14 @@ def set_style(root): "TCombobox", foreground="#000000", background="#555555", - borderwitdh=0, + borderwidth=0, arrowsize=24 ) style.map( "TCombobox", background=[("active", "#666666"), ("selected", "#383838")], - fieldbackground=[("readonly", "#A9A9A9")] + fieldbackground=[("readonly", "#A9A9A9")], + borderwidth=[("active", 0)], ) root.option_add("*TCombobox*Listbox*Font", (None, 18)) root.option_add("*TCombobox*Listbox.foreground", "#000000") diff --git a/brownie/gui/textbook.py b/brownie/gui/textbook.py index 8b9d8ddd3..e05eefb4a 100755 --- a/brownie/gui/textbook.py +++ b/brownie/gui/textbook.py @@ -177,6 +177,7 @@ def __init__(self, root, text): tabs=tkFont.Font(font=text['font']).measure(' '), wrap="none" ) + self._line_no.config(background="#272727") self._text.bind('', root._search) def __getattr__(self, attr): From 7a9074dc4ca78e81b955d87bd5257e7b0575265b Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 02:57:39 +0300 Subject: [PATCH 53/72] add tooltips --- brownie/gui/root.py | 7 ++++++- brownie/gui/tooltip.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index 642ddb265..f07f6db33 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -8,9 +8,10 @@ from .buttons import ScopingToggle, ConsoleToggle, HighlightsToggle from .listview import ListView -from .textbook import TextBook from .select import ContractSelect, ReportSelect from .styles import set_style, TEXT_STYLE +from .textbook import TextBook +from .tooltip import ToolTip from brownie.project.build import Build from brownie._config import CONFIG @@ -91,18 +92,22 @@ def __init__(self, root): self.combo = ContractSelect(self, [k for k, v in build.items() if v['bytecode']]) self.combo.pack(side="right", anchor="e") self.combo.configure(width=23) + ToolTip(self.combo, "Select the contract source to view") path = Path(CONFIG['folders']['project']).joinpath('reports') self.report = ReportSelect(self, list(path.glob('**/*.json'))) self.report.pack(side="right", anchor="e", padx=10) self.report.configure(width=23) + ToolTip(self.report, "Select a report to overlay onto source code") self.scope = ScopingToggle(self) self.scope.pack(side="left") + ToolTip(self.scope, "Filter opcodes to only show those\nrelated to the highlighted source") self.console = ConsoleToggle(self) self.console.pack(side="left") self.highlight = HighlightsToggle(self) self.highlight.pack(side="left") + ToolTip(self.highlight, "Toggle report highlighting") diff --git a/brownie/gui/tooltip.py b/brownie/gui/tooltip.py index 0d7b4d33d..e4a974e1c 100755 --- a/brownie/gui/tooltip.py +++ b/brownie/gui/tooltip.py @@ -2,13 +2,13 @@ import tkinter as tk -TOOLTIP_DELAY = 0.5 +TOOLTIP_DELAY = 1 class ToolTip(tk.Toplevel): - def __init__(self, root, widget, text=None, textvariable=None): - super().__init__(root) + def __init__(self, widget, text=None, textvariable=None): + super().__init__(widget._root()) label = tk.Label(self, text=text, textvariable=textvariable, font=(None, 10)) label.pack() self.wm_overrideredirect(True) @@ -20,12 +20,16 @@ def __init__(self, root, widget, text=None, textvariable=None): def enter(self, event): self.kill = False self.widget.bind("", self.leave) + self.widget.bind("<1>", self.leave) self.after(int(TOOLTIP_DELAY*1000), self.show) def show(self): if self.kill: return - self.geometry("+{}+{}".format(self.winfo_pointerx()+5, self.winfo_pointery()+5)) + self.geometry("+{}+{}".format( + self.winfo_pointerx()+5, + self.winfo_pointery()+5 + )) self.lift() self.deiconify() From e27ed9924b11e9a6d72c024a0c8982908477f33e Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 03:01:42 +0300 Subject: [PATCH 54/72] comment out console - will reimplement with static analysis tools --- brownie/gui/root.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/brownie/gui/root.py b/brownie/gui/root.py index f07f6db33..ac176d76e 100755 --- a/brownie/gui/root.py +++ b/brownie/gui/root.py @@ -6,10 +6,17 @@ import tkinter as tk from tkinter import ttk -from .buttons import ScopingToggle, ConsoleToggle, HighlightsToggle +from .buttons import ( + ScopingToggle, + # ConsoleToggle, + HighlightsToggle +) from .listview import ListView from .select import ContractSelect, ReportSelect -from .styles import set_style, TEXT_STYLE +from .styles import ( + set_style, + # TEXT_STYLE +) from .textbook import TextBook from .tooltip import ToolTip @@ -68,6 +75,7 @@ class MainFrame(ttk.Frame): def __init__(self, root): super().__init__(root) + self.oplist = ListView(self, (("pc", 80), ("opcode", 200))) self.oplist.configure(height=30) self.oplist.pack(side="right", fill="y", expand=True) @@ -77,9 +85,11 @@ def __init__(self, root): self.note = TextBook(frame) self.note.pack(side="top", fill="both", expand=True) self.note.configure(width=920, height=100) - self.console = tk.Text(frame, height=1) - self.console.pack(side="bottom", fill="both") - self.console.configure(**TEXT_STYLE) + + # GUI console - will be implemented later! + # self.console = tk.Text(frame, height=1) + # self.console.pack(side="bottom", fill="both") + # self.console.configure(**TEXT_STYLE) class ToolbarFrame(ttk.Frame): @@ -105,8 +115,8 @@ def __init__(self, root): self.scope.pack(side="left") ToolTip(self.scope, "Filter opcodes to only show those\nrelated to the highlighted source") - self.console = ConsoleToggle(self) - self.console.pack(side="left") + # self.console = ConsoleToggle(self) + # self.console.pack(side="left") self.highlight = HighlightsToggle(self) self.highlight.pack(side="left") From 31755cd7a864f43f79e0ae33450df66b7db6fd7d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 03:40:57 +0300 Subject: [PATCH 55/72] bugfixes --- brownie/cli/init.py | 2 +- brownie/project/compiler.py | 2 +- brownie/test/coverage.py | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/brownie/cli/init.py b/brownie/cli/init.py index 24fba7a9f..3fad9caae 100644 --- a/brownie/cli/init.py +++ b/brownie/cli/init.py @@ -22,9 +22,9 @@ build/ Compiled contracts and test data contracts/ Contract source code +reports/ Report files for contract analysis scripts/ Scripts for deployment and interaction tests/ Scripts for project testing -reports/ Report files for contract analysis brownie-config.json Project configuration file""" diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 938dce29d..f014ce1e2 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -136,7 +136,7 @@ def _generate_pcMap(compiled): bytecode = compiled['contracts'][filename][name]['evm']['deployedBytecode'] if not bytecode['object']: - compiled['contracts'][filename][name]['evm']['pcMap'] = [] + compiled['contracts'][filename][name]['evm']['pcMap'] = {} continue opcodes = bytecode['opcodes'] source_map = bytecode['sourceMap'] diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index e4a167917..ca1483dfc 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -89,6 +89,9 @@ def merge_coverage(coverage_files): for source, fn_name in [(k, x) for k, v in coverage[contract_name].items() for x in v]: f = merged_eval[contract_name][source][fn_name] c = coverage[contract_name][source][fn_name] + if not f['pct']: + f.update(c) + continue if not c['pct'] or f == c: continue if f['pct'] == 1 or c['pct'] == 1: From d1e8aaf28722ba4e1cea68cb2261d0a2330adefd Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 03:53:01 +0300 Subject: [PATCH 56/72] increment coverage report filenames --- brownie/cli/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 44cd2b2f9..f867dc8d0 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -135,8 +135,10 @@ def main(): if args['--coverage']: coverage_eval = merge_coverage(coverage_files) display_report(coverage_eval) - filename = "reports/coverage-{}.json".format(time.strftime('%d%m%y')) - path = Path(CONFIG['folders']['project']).joinpath(filename) + filename = "coverage-"+time.strftime('%d%m%y')+"{}.json" + path = Path(CONFIG['folders']['project']).joinpath('reports') + count = len(list(path.glob(filename.format('*')))) + path = path.joinpath(filename.format("-"+str(count) if count else "")) json.dump( generate_report(coverage_eval), path.open('w'), From 4244def49cdee00654e00a83fd6455a3cfa1bc39 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 03:53:39 +0300 Subject: [PATCH 57/72] update docs --- docs/coverage.rst | 63 +++++++++++++++++++++++++++------------------- docs/init.rst | 1 + docs/opview.png | Bin 162257 -> 159447 bytes 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/docs/coverage.rst b/docs/coverage.rst index bbdcbd660..7fcb0cd48 100644 --- a/docs/coverage.rst +++ b/docs/coverage.rst @@ -4,7 +4,7 @@ Checking Test Coverage ====================== -.. warning:: Test coverage evaluation is still under development. There may be undiscovered issues, particularly cases where conditional True/False evaluation is incorrect. Use common sense when viewing coverage reports and please open an issue on github if you encounter any issues. +.. warning:: Test coverage evaluation is still under development. There may be undiscovered issues, particularly cases where conditional ``True``/``False`` evaluation is incorrect. Use common sense when viewing coverage reports and please open an issue on github if you encounter any issues. Test coverage is estimated by generating a map of opcodes associated with each function and line of the smart contract source code, and then analyzing the stack trace of each transaction to see which opcodes were executed. @@ -21,39 +21,50 @@ This will run all the test scripts in the ``tests/`` folder and give an estimate :: $ brownie test --coverage - Using network 'development' - Running 'ganache-cli -a 20'... + Brownie v1.0.0 - Python development framework for Ethereum + + Using solc version v0.5.7 Running transfer.py - 1 test - ✓ Deployment 'token' (0.1882s) - ✓ Transfer tokens (0.1615s) - Using network 'development' - Running 'ganache-cli -a 20'... + ✓ 0 - setup (0.1882s) + ✓ 1 - Transfer tokens (0.1615s) + ✓ 2 - Evaluating test coverage (0.0009s) Running approve_transferFrom.py - 3 tests - ✓ Deployment 'token' (0.1263s) - ✓ Set approval (0.2016s) - ✓ Transfer tokens with transferFrom (0.1375s) - ✓ transerFrom should revert (0.0486s) + ✓ 0 - setup (0.1263s) + ✓ 1 - Set approval (0.2016s) + ✓ 2 - Transfer tokens with transferFrom (0.1375s) + ✓ 3 - transerFrom should revert (0.0486s) + ✓ 4 - Evaluating test coverage (0.0026s) + + SUCCESS: All tests passed. Coverage analysis complete! - contract: Token - add - 50.0% - allowance - 0.0% - approve - 100.0% - balanceOf - 0.0% - decimals - 0.0% - name - 0.0% - sub - 75.0% - symbol - 0.0% - totalSupply - 0.0% - transfer - 100.0% - transferFrom - 100.0% + contract: Token + SafeMath.add - 66.7% + SafeMath.sub - 100.0% + Token. - 0.0% + Token.allowance - 100.0% + Token.approve - 100.0% + Token.balanceOf - 100.0% + Token.decimals - 0.0% + Token.name - 100.0% + Token.symbol - 0.0% + Token.totalSupply - 100.0% + Token.transfer - 85.7% + Token.transferFrom - 100.0% + + Coverage report saved at reports/coverage-010170.json + +Brownie will output a % score for each contract method, that you can use to quickly gauge your overall coverage level. A coverage report is also saved in the project's ``reports`` folder. + +.. _coverage-gui: -Brownie will output a % score for each contract method, that you can use to quickly gauge your overall coverage level. +Brownie GUI +=========== -To analyze specific test coverage, type: +For an in-depth look at your test coverage, type: :: @@ -65,7 +76,7 @@ Or from the console: >>> Gui() -This will open the Brownie GUI. Then press ``C`` to display the coverage results. Relevant code will be highlighted in different colors: +This will open the Brownie GUI. In the upper right drop boxes, select a contract to view and then choose the generated coverage report JSON. Relevant code will be highlighted in different colors: * Green - code was executed during the tests * Yellow - code was executed, but only evaluated truthfully diff --git a/docs/init.rst b/docs/init.rst index 6713dc06c..78ebd2cc6 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -12,6 +12,7 @@ This will create the following project structure within the folder: * ``build/``: Compiled contracts and test data * ``contracts/``: Contract source code +* ``reports/``: JSON report files for use in the :ref:`coverage-gui` * ``scripts/``: Scripts for deployment and interaction * ``tests/``: Scripts for testing your project * ``brownie-config.json``: Configuration file for the project diff --git a/docs/opview.png b/docs/opview.png index afccbbe862a920ca127b98c82c1b3d376889bd59..6732ab18900b342210880f3c866f389a738e4f5c 100644 GIT binary patch literal 159447 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%0A%VPkNcut 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 z z5`471C`IjjlpcMED$4oW8#@WS5`cIwhVtLso`!kYvfTDoZU#|{4?5^RVz~dA08xkn zm~Vm+`=_a7zk)g%x!n3rTlNPy!a9f6bi(2armDp7RD=JPdQGW>Z3dRP9rtz)MniOP z-e<=nUl5Pg|3Y=9c8M6LGqt9_W!!4<0JtRfx;sdiR_4~r9&US0v!pFu+wW2aO(ht^ zHy}apN;ka=s2G5gBAy?RHb$itqW}`8PYV0(@jENh2BrzdeotrRps_IM~Xrc?jZv!&p}e-NTjrp3yQlvR#39Qpq{ z90LbPwA2n&Gi(s%sfy;Mjj8au`yu2PS*C=%-89JD?I7FX_;SRzh_5M)X?$Bkl)8_H zlYw!BB$RZHr_R0b|B}i*fJYA=)}J53SP9<=ebcTsRX3)Pb<^LgZE$#M!p0Ke@w*idcx zI5I~wV!g7Gm#_CGQWDVc_xAf>0h*;{ibl^huJ`waX}`jUj9aNU{?XJC?)1QZzZt1X z&wc7#x{b@Z)^=NJ%-V^Lf(VB~C!VjyJMRgKr?)p3N*9*Mi-$i-#^wa)M+tZ+75QaL zCe}M_BwCxR?4LpVJeR(Ca{R97a2;UFrGGS-y$kPHy_O25KE5$LK*6+Y6Hy1DB2*&QA(GQd;>liP$`v0Sn5XKM?C%<44PCG3(w z_FWV-no3≧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

Cz=xbax$i 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 VaP@1YodAdzLIbH@s(SCqzX20+CV&6{ literal 162257 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 z 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%0DHuPym47%#`7&zf40KuVl;xRtB%r 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 Date: Sun, 12 May 2019 16:11:06 +0300 Subject: [PATCH 58/72] show combined coverage % for entire contract --- brownie/cli/test.py | 39 ++++++++++++++++++++++++------------- brownie/project/compiler.py | 16 +++++++++++++-- brownie/test/coverage.py | 14 +++++++++---- 3 files changed, 49 insertions(+), 20 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index f867dc8d0..1ccc4ed81 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -30,6 +30,9 @@ (1, "bright green") ] +WARN = "{0[error]}WARNING{0}: ".format(color) +ERROR = "{0[error]}ERROR{0}: ".format(color) + __doc__ = """Usage: brownie test [] [] [options] Arguments: @@ -66,9 +69,9 @@ def main(): else: idx = slice(int(idx)-1, int(idx)) except Exception: - sys.exit("{0[error]}ERROR{0}: Invalid range. Must be an integer or slice (eg. 1:4)".format(color)) + sys.exit(ERROR+"Invalid range. Must be an integer or slice (eg. 1:4)") elif args['']: - sys.exit("{0[error]}ERROR:{0} Cannot specify a range when running multiple tests files.".format(color)) + sys.exit(ERROR+"Cannot specify a range when running multiple tests files.") else: idx = slice(0, None) @@ -123,8 +126,10 @@ def main(): sys.exit() finally: if traceback_info: - print("\n{0[error]}WARNING{0}: {1} test{2} failed.{0}".format( - color, len(traceback_info), "s" if len(traceback_info) > 1 else "" + print("\n{0}{1} test{2} failed.".format( + WARN, + len(traceback_info), + "s" if len(traceback_info) > 1 else "" )) for err in traceback_info: print("\nException info for {0[0]}:\n{0[1]}".format(err)) @@ -134,13 +139,14 @@ def main(): if args['--coverage']: coverage_eval = merge_coverage(coverage_files) + report = generate_report(coverage_eval) display_report(coverage_eval) filename = "coverage-"+time.strftime('%d%m%y')+"{}.json" path = Path(CONFIG['folders']['project']).joinpath('reports') count = len(list(path.glob(filename.format('*')))) path = path.joinpath(filename.format("-"+str(count) if count else "")) json.dump( - generate_report(coverage_eval), + report, path.open('w'), sort_keys=True, indent=2, @@ -150,8 +156,9 @@ def main(): if args['--gas']: print('\nGas Profile:') - for i in sorted(transaction.gas_profile): - print("{0} - avg: {1[avg]:.0f} low: {1[low]} high: {1[high]}".format(i, transaction.gas_profile[i])) + gas = transaction.gas_profile + for i in sorted(gas): + print("{0} - avg: {1[avg]:.0f} low: {1[low]} high: {1[high]}".format(i, gas[i])) def get_test_files(path): @@ -164,7 +171,7 @@ def get_test_files(path): if not path.suffix: path = Path(str(path)+".py") if not path.exists(): - sys.exit("{0[error]}ERROR{0}: Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) + sys.exit(ERROR+"Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) result = [path] else: result = [i for i in path.glob('**/*.py') if i.name[0] != "_" and "/_" not in str(i)] @@ -187,7 +194,7 @@ def run_test(filename, network, idx): )) if not test_names: - print("\n{0[error]}WARNING{0}: No test functions in {0[module]}{1}.py{0}".format(color, filename)) + print("\n{0}No test functions in {1[module]}{2}.py{1}".format(WARN, color, filename)) return [], {} if ARGV['coverage']: @@ -199,7 +206,9 @@ def run_test(filename, network, idx): test_names.remove('setup') fn, default_args = _get_fn(module, 'setup') - if default_args['skip'] is True or (default_args['skip'] == "coverage" and ARGV['coverage']): + if default_args['skip'] is True or ( + default_args['skip'] == "coverage" and ARGV['coverage'] + ): return [], {} p = TestPrinter(filename, 0, len(test_names)) traceback_info += run_test_method(fn, default_args, p) @@ -218,7 +227,7 @@ def run_test(filename, network, idx): if ARGV['coverage']: ARGV['always_transact'] = always_transact if traceback_info and traceback_info[-1][2] == ReadTimeout: - print("{0[error]}WARNING{0}: RPC crashed, terminating test".format(color)) + print(WARN+"RPC crashed, terminating test") network.rpc.kill(False) network.rpc.launch(CONFIG['active_network']['test-rpc']) break @@ -272,9 +281,11 @@ def run_test_method(fn, args, p): def display_report(coverage_eval): print("\nCoverage analysis:\n") - for contract in coverage_eval: - print(" contract: {0[contract]}{1}{0}".format(color, contract)) - for fn_name, pct in [(x, v[x]['pct']) for v in coverage_eval[contract].values() for x in v]: + for name in coverage_eval: + pct = coverage_eval[name].pop('pct') + c = next(i[1] for i in COVERAGE_COLORS if pct <= i[0]) + print(" contract: {0[contract]}{1}{0} - {2}{3:.1%}{0}".format(color, name, color(c), pct)) + for fn_name, pct in [(x, v[x]['pct']) for v in coverage_eval[name].values() for x in v]: c = next(i[1] for i in COVERAGE_COLORS if pct <= i[0]) print(" {0[contract_method]}{1}{0} - {2}{3:.1%}{0}".format( color, fn_name, color(c), pct diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index f014ce1e2..d9765aad9 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -95,9 +95,10 @@ def _compile_and_format(input_json): n[:36], evm['bytecode']['object'][loc+40:] ) + all_paths = sorted(set(v['contract'] for v in evm['pcMap'].values() if v['contract'])) result[name] = { 'abi': data['abi'], - 'allSourcePaths': sorted(set(v['contract'] for v in evm['pcMap'].values() if v['contract'])), + 'allSourcePaths': all_paths, 'ast': compiled['sources'][filename]['ast'], 'bytecode': evm['bytecode']['object'], 'bytecodeSha1': sha1(evm['bytecode']['object'][:-68].encode()).hexdigest(), @@ -115,6 +116,7 @@ def _compile_and_format(input_json): 'type': sources.get_type(name) } result[name]['coverageMap'] = _generate_coverageMap(result[name]) + result[name]['coverageMapTotals'] = _generate_coverageMapTotals(result[name]['coverageMap']) return result @@ -223,6 +225,16 @@ def _generate_coverageMap(build): return final +def _generate_coverageMapTotals(coverage_map): + totals = {'total': 0} + for path, fn_name in [(k, x) for k, v in coverage_map.items() for x in v]: + maps = coverage_map[path][fn_name] + count = len([i for i in maps if not i['jump']]) + len([i for i in maps if i['jump']])*2 + totals[fn_name] = count + totals['total'] += count + return totals + + def _isolate_lines(compiled): '''Identify line based coverage map items. @@ -234,7 +246,7 @@ def _isolate_lines(compiled): line_map = {} # find all the JUMPI opcodes - for i in [k for k,v in pcMap.items() if v['contract'] and v['op']=="JUMPI"]: + for i in [k for k, v in pcMap.items() if v['contract'] and v['op'] == "JUMPI"]: op = pcMap[i] if op['contract'] not in line_map: line_map[op['contract']] = [] diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index ca1483dfc..1380aea4d 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -83,6 +83,7 @@ def merge_coverage(coverage_files): continue coverage = json.load(path.open())['coverage'] for contract_name in list(coverage): + del coverage[contract_name]['pct'] if contract_name not in merged_eval: merged_eval[contract_name] = coverage.pop(contract_name) continue @@ -108,6 +109,7 @@ def merge_coverage(coverage_files): def _calculate_pct(coverage_eval): '''Internal method to calculate coverage percentages''' for name in coverage_eval: + contract_count = 0 coverage_map = build[name]['coverageMap'] for path, fn_name in [(k, x) for k, v in coverage_map.items() for x in v]: result = coverage_eval[name][path] @@ -115,12 +117,13 @@ def _calculate_pct(coverage_eval): result[fn_name] = {'pct': 0} continue if 'pct' in result[fn_name] and result[fn_name]['pct'] in (0, 1): + if result[fn_name]['pct']: + contract_count += build[name]['coverageMapTotals'][fn_name] result[fn_name] = {'pct': result[fn_name]['pct']} continue result = result[fn_name] count = 0 maps = coverage_map[path][fn_name] - total = len([i for i in maps if i['jump']])*2 + len([i for i in maps if not i['jump']]) for idx, item in enumerate(maps): if idx in result['tx']: count += 2 if item['jump'] else 1 @@ -129,9 +132,12 @@ def _calculate_pct(coverage_eval): continue if idx in result['true'] or idx in result['false']: count += 1 - result['pct'] = round(count / total, 4) + contract_count += count + result['pct'] = round(count / build[name]['coverageMapTotals'][fn_name], 4) if result['pct'] == 1: coverage_eval[name][path][fn_name] = {'pct': 1} + pct = round(contract_count / build[name]['coverageMapTotals']['total'], 4) + coverage_eval[name]['pct'] = pct return coverage_eval @@ -144,8 +150,8 @@ def generate_report(coverage_eval): } for name, coverage in coverage_eval.items(): report['highlights'][name] = {} - report['coverage'][name] = {} - for path in coverage: + report['coverage'][name] = {'pct': coverage['pct']} + for path in [i for i in coverage if i != "pct"]: coverage_map = build[name]['coverageMap'][path] report['highlights'][name][path] = [] for fn_name, lines in coverage_map.items(): From 75560f8ed8991feece05bfaf93456bae3c510666 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 16:20:42 +0300 Subject: [PATCH 59/72] show time to complete entire test module --- brownie/cli/test.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 1ccc4ed81..7dc84fb2f 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -231,12 +231,13 @@ def run_test(filename, network, idx): network.rpc.kill(False) network.rpc.launch(CONFIG['active_network']['test-rpc']) break + coverage_eval = {} if not traceback_info and ARGV['coverage']: p.start("Evaluating test coverage") coverage_eval = analyze_coverage(TxHistory().copy()) p.stop() - return traceback_info, coverage_eval - return traceback_info, {} + p.finish() + return traceback_info, coverage_eval def _get_fn(module, name): @@ -299,6 +300,7 @@ def __init__(self, path, count, total): self.path = path self.count = count self.total = total + self.total_time = time.time() print("\nRunning {0[module]}{1}.py{0} - {2} test{3}".format( color, path, @@ -336,12 +338,20 @@ def stop(self, err=None, expect=False): self._print(msg, symbol, color_str, "dull") self.count += 1 + def finish(self): + print("Completed {0[module]}{1}.py{0} ({2:.4f}s)".format( + color, + self.path, + time.time() - self.total_time + )) + def _print(self, msg, symbol=" ", symbol_color="success", main_color=None): - sys.stdout.write("\r {}{}{} {} - {}".format( + sys.stdout.write("\r {}{}{} {} - {}{}".format( color(symbol_color), symbol, color(main_color), self.count, - msg + msg, + color )) sys.stdout.flush() From 5e237e5040b6134a39ccdbe127a4525183ce53e4 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 16:41:37 +0300 Subject: [PATCH 60/72] colors --- brownie/cli/test.py | 8 +++++--- brownie/cli/utils/color.py | 5 +++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 7dc84fb2f..e2a1864a8 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -132,7 +132,7 @@ def main(): "s" if len(traceback_info) > 1 else "" )) for err in traceback_info: - print("\nException info for {0[0]}:\n{0[1]}".format(err)) + print("\nTraceback for {0[0]}:\n{0[1]}".format(err)) sys.exit() print("\n{0[success]}SUCCESS{0}: All tests passed.".format(color)) @@ -270,11 +270,13 @@ def run_test_method(fn, args, p): p.stop(e, args['pending']) if type(e) != ExpectedFailing and args['pending']: return [] + path = Path(sys.modules[fn.__module__].__file__).relative_to(CONFIG['folders']['project']) + path = "{0[module]}{1}.{0[callable]}{2}{0}".format(color, str(path)[:-3], fn.__name__) return [( - fn.__name__, + path, color.format_tb( sys.exc_info(), - Path(sys.modules[fn.__module__].__file__).relative_to(CONFIG['folders']['project']) + sys.modules[fn.__module__].__file__, ), type(e) )] diff --git a/brownie/cli/utils/color.py b/brownie/cli/utils/color.py index 4211dfd61..578290b86 100755 --- a/brownie/cli/utils/color.py +++ b/brownie/cli/utils/color.py @@ -123,7 +123,7 @@ def _write(self, value): else: sys.stdout.write('{0[value]}{1}{0[dull]}'.format(self, value)) - def format_tb(self, exc, filename = None, start = None, stop = None): + def format_tb(self, exc, filename=None, start=None, stop=None): tb = [i.replace("./", "") for i in traceback.format_tb(exc[2])] if filename and not ARGV['tb']: try: @@ -134,6 +134,7 @@ def format_tb(self, exc, filename = None, start = None, stop = None): pass for i in range(len(tb)): info, code = tb[i].split('\n')[:2] + info = info.replace(CONFIG['folders']['project'], ".") info = [x.strip(',') for x in info.strip().split(' ')] if 'site-packages/' in info[1]: info[1] = '"'+info[1].split('site-packages/')[1] @@ -147,4 +148,4 @@ def _check_dict(value): def _check_list(value): - return type(value) in (list, tuple) or hasattr(value, "_print_as_list") \ No newline at end of file + return type(value) in (list, tuple) or hasattr(value, "_print_as_list") From 0720d315463e0a99603ee72810973e47abff3883 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 16:46:43 +0300 Subject: [PATCH 61/72] total runtime across all tests --- brownie/cli/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index e2a1864a8..e16c234ef 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -79,6 +79,7 @@ def main(): coverage_files = [] traceback_info = [] + start_time = time.time() try: for filename in test_files: coverage_json = Path(CONFIG['folders']['project']) @@ -125,8 +126,9 @@ def main(): print("\n\nTest execution has been terminated by KeyboardInterrupt.") sys.exit() finally: + print("\nTotal runtime: {:.4}s".format(time.time() - start_time)) if traceback_info: - print("\n{0}{1} test{2} failed.".format( + print("{0}{1} test{2} failed.".format( WARN, len(traceback_info), "s" if len(traceback_info) > 1 else "" @@ -135,7 +137,7 @@ def main(): print("\nTraceback for {0[0]}:\n{0[1]}".format(err)) sys.exit() - print("\n{0[success]}SUCCESS{0}: All tests passed.".format(color)) + print("{0[success]}SUCCESS{0}: All tests passed.".format(color)) if args['--coverage']: coverage_eval = merge_coverage(coverage_files) From 9605fe9672b029bfa8b5fb32265e280c3d1897c9 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 17:30:54 +0300 Subject: [PATCH 62/72] refactor WIP - get_test_data - currently broken --- brownie/cli/test.py | 70 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index e16c234ef..ead2e784a 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -60,8 +60,9 @@ def main(): history = TxHistory() history._revert_lock = True - test_files = get_test_files(args['']) - if len(test_files) == 1 and args['']: + test_paths = get_test_files(args['']) + + if len(test_paths) == 1 and args['']: try: idx = args[''] if ':' in idx: @@ -75,16 +76,23 @@ def main(): else: idx = slice(0, None) + coverage_files, test_data = get_test_data(test_paths) + + # TODO from here + # if test_data: + # test_data = [(path, coverage_json, [method names], default_args),...] + # only run tests if test_data exists + # need to refactor run_test since all the data is already there now + network.connect(ARGV['network']) - coverage_files = [] + traceback_info = [] start_time = time.time() try: - for filename in test_files: + for filename in test_paths: coverage_json = Path(CONFIG['folders']['project']) coverage_json = coverage_json.joinpath("build/coverage"+filename[5:]+".json") - coverage_files.append(coverage_json) if coverage_json.exists(): coverage_eval = json.load(coverage_json.open())['coverage'] if ARGV['update'] and (coverage_eval or not ARGV['coverage']): @@ -169,15 +177,49 @@ def get_test_files(path): if path[:6] != "tests/": path = "tests/" + path path = Path(CONFIG['folders']['project']).joinpath(path) - if not path.is_dir(): - if not path.suffix: - path = Path(str(path)+".py") - if not path.exists(): - sys.exit(ERROR+"Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) - result = [path] - else: - result = [i for i in path.glob('**/*.py') if i.name[0] != "_" and "/_" not in str(i)] - return [str(i.relative_to(CONFIG['folders']['project']))[:-3] for i in result] + if path.is_dir(): + return [i for i in path.glob('**/*.py') if i.name[0] != "_" and "/_" not in str(i)] + if not path.suffix: + path = Path(str(path)+".py") + if not path.exists(): + sys.exit(ERROR+"Cannot find {0[module]}tests/{1}{0}".format(color, path.name)) + return [path] + + +def get_test_data(test_paths): + coverage_files = [] + test_data = [] + project = Path(CONFIG['folders']['project']) + for path in test_paths: + coverage_json = project.joinpath("build/coverage/"+path.stem+".json") + coverage_files.append(coverage_json) + if coverage_json.exists(): + coverage_eval = json.load(coverage_json.open())['coverage'] + if ARGV['update'] and (coverage_eval or not ARGV['coverage']): + continue + else: + coverage_eval = {} + for p in list(coverage_json.parents)[::-1]: + p.mkdir(exist_ok=True) + module_name = str(path.relative_to(project))[:-3].replace(os.sep, '.') + module = importlib.import_module(module_name) + test_names = re.findall(r'\ndef[\s ]{1,}([^_]\w*)[\s ]*\([^)]*\)', path.open().read()) + duplicates = set([i for i in test_names if test_names.count(i) > 1]) + if duplicates: + raise ValueError("{} contains multiple test methods of the same name: {}".format( + path.relative_to(project), + ", ".join(duplicates) + )) + if 'setup' in test_names: + fn, default_args = _get_fn(module, 'setup') + if default_args['skip'] is True or ( + default_args['skip'] == "coverage" and ARGV['coverage'] + ): + continue + else: + default_args = FalseyDict() + test_data.append((path, coverage_eval, test_names, default_args)) + return coverage_files, test_data def run_test(filename, network, idx): From 05fd37f67b186878169f3fa33bda9217c60e275e Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 20:31:28 +0300 Subject: [PATCH 63/72] major refactor, show count/total for all test modules - closes #65 --- brownie/cli/test.py | 269 ++++++++++++++++++++------------------------ 1 file changed, 121 insertions(+), 148 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index ead2e784a..0161ad54f 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -60,7 +60,8 @@ def main(): history = TxHistory() history._revert_lock = True - test_paths = get_test_files(args['']) + test_paths = get_test_paths(args['']) + coverage_files, test_data = get_test_data(test_paths) if len(test_paths) == 1 and args['']: try: @@ -69,109 +70,27 @@ def main(): idx = slice(*[int(i)-1 for i in idx.split(':')]) else: idx = slice(int(idx)-1, int(idx)) + if 'setup' in test_data[0][4]: + test_data[0][4].remove('setup') + test_data[0][4] = ['setup'] + test_data[0][4][idx] + else: + test_data[0][4] = ['setup'] + test_data[0][4][idx] except Exception: sys.exit(ERROR+"Invalid range. Must be an integer or slice (eg. 1:4)") elif args['']: sys.exit(ERROR+"Cannot specify a range when running multiple tests files.") - else: - idx = slice(0, None) - - coverage_files, test_data = get_test_data(test_paths) - - # TODO from here - # if test_data: - # test_data = [(path, coverage_json, [method names], default_args),...] - # only run tests if test_data exists - # need to refactor run_test since all the data is already there now - - network.connect(ARGV['network']) - - traceback_info = [] - - start_time = time.time() - try: - for filename in test_paths: - coverage_json = Path(CONFIG['folders']['project']) - coverage_json = coverage_json.joinpath("build/coverage"+filename[5:]+".json") - if coverage_json.exists(): - coverage_eval = json.load(coverage_json.open())['coverage'] - if ARGV['update'] and (coverage_eval or not ARGV['coverage']): - continue - else: - coverage_eval = {} - for p in list(coverage_json.parents)[::-1]: - if not p.exists(): - p.mkdir() - - tb, cov = run_test(filename, network, idx) - if tb: - traceback_info += tb - if coverage_json.exists(): - coverage_json.unlink() - continue - - if ARGV['coverage']: - coverage_eval = cov - - build_files = set(Path('build/contracts/{}.json'.format(i)) for i in coverage_eval) - coverage_eval = { - 'coverage': coverage_eval, - 'sha1': dict((str(i), Build()[i.stem]['bytecodeSha1']) for i in build_files) - } - if args['']: - continue - - test_path = Path(filename+".py") - coverage_eval['sha1'][str(test_path)] = get_ast_hash(test_path) - json.dump( - coverage_eval, - coverage_json.open('w'), - sort_keys=True, - indent=2, - default=sorted - ) - except KeyboardInterrupt: - print("\n\nTest execution has been terminated by KeyboardInterrupt.") - sys.exit() - finally: - print("\nTotal runtime: {:.4}s".format(time.time() - start_time)) - if traceback_info: - print("{0}{1} test{2} failed.".format( - WARN, - len(traceback_info), - "s" if len(traceback_info) > 1 else "" - )) - for err in traceback_info: - print("\nTraceback for {0[0]}:\n{0[1]}".format(err)) - sys.exit() - - print("{0[success]}SUCCESS{0}: All tests passed.".format(color)) - if args['--coverage']: - coverage_eval = merge_coverage(coverage_files) - report = generate_report(coverage_eval) - display_report(coverage_eval) - filename = "coverage-"+time.strftime('%d%m%y')+"{}.json" - path = Path(CONFIG['folders']['project']).joinpath('reports') - count = len(list(path.glob(filename.format('*')))) - path = path.joinpath(filename.format("-"+str(count) if count else "")) - json.dump( - report, - path.open('w'), - sort_keys=True, - indent=2, - default=sorted - ) - print("Coverage report saved at {}".format(path.relative_to(CONFIG['folders']['project']))) - - if args['--gas']: - print('\nGas Profile:') - gas = transaction.gas_profile - for i in sorted(gas): - print("{0} - avg: {1[avg]:.0f} low: {1[low]} high: {1[high]}".format(i, gas[i])) + if not test_data: + print("No tests to run.") + else: + run_test_modules(test_data, not args['']) + if ARGV['coverage']: + display_report(coverage_files, not args['']) + if ARGV['gas']: + display_gas_profile() -def get_test_files(path): +def get_test_paths(path): if not path: path = "" if path[:6] != "tests/": @@ -204,6 +123,9 @@ def get_test_data(test_paths): module_name = str(path.relative_to(project))[:-3].replace(os.sep, '.') module = importlib.import_module(module_name) test_names = re.findall(r'\ndef[\s ]{1,}([^_]\w*)[\s ]*\([^)]*\)', path.open().read()) + if not test_names: + print("\n{0}No test functions in {1[module]}{2}.py{1}".format(WARN, color, path)) + continue duplicates = set([i for i in test_names if test_names.count(i) > 1]) if duplicates: raise ValueError("{} contains multiple test methods of the same name: {}".format( @@ -211,39 +133,68 @@ def get_test_data(test_paths): ", ".join(duplicates) )) if 'setup' in test_names: - fn, default_args = _get_fn(module, 'setup') - if default_args['skip'] is True or ( - default_args['skip'] == "coverage" and ARGV['coverage'] - ): + fn, args = _get_fn(module, 'setup') + if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): continue - else: - default_args = FalseyDict() - test_data.append((path, coverage_eval, test_names, default_args)) + test_data.append((module, coverage_json, coverage_eval, test_names)) return coverage_files, test_data -def run_test(filename, network, idx): - network.rpc.reset() - if type(CONFIG['test']['gas_limit']) is int: - network.gas_limit(CONFIG['test']['gas_limit']) +def run_test_modules(test_data, save): + TestPrinter.grand_total = len(test_data) + count = sum([len([x for x in i[3] if x != "setup"]) for i in test_data]) + print("Running {} tests across {} modules.".format(count, len(test_data))) + network.connect(ARGV['network']) + traceback_info = [] + start_time = time.time() + try: + for (module, coverage_json, coverage_eval, test_names) in test_data: + tb, cov = run_test(module, network, test_names) + if tb: + traceback_info += tb + if coverage_json.exists(): + coverage_json.unlink() + continue - module = importlib.import_module(filename.replace(os.sep, '.')) - code = Path(filename+".py").open().read() - test_names = re.findall(r'\ndef[\s ]{1,}([^_]\w*)[\s ]*\([^)]*\)', code) - duplicates = set([i for i in test_names if test_names.count(i) > 1]) - if duplicates: - raise ValueError("tests/{}.py contains multiple tests of the same name: {}".format( - filename, - ", ".join(duplicates) - )) + if not save: + continue + if ARGV['coverage']: + coverage_eval = cov + + build_files = set(Path('build/contracts/{}.json'.format(i)) for i in coverage_eval) + coverage_eval = { + 'coverage': coverage_eval, + 'sha1': dict((str(i), Build()[i.stem]['bytecodeSha1']) for i in build_files) + } + coverage_eval['sha1'][module.__file__] = get_ast_hash(module.__file__) + json.dump( + coverage_eval, + coverage_json.open('w'), + sort_keys=True, + indent=2, + default=sorted + ) + except KeyboardInterrupt: + print("\n\nTest execution has been terminated by KeyboardInterrupt.") + sys.exit() + finally: + print("\nTotal runtime: {:.4}s".format(time.time() - start_time)) + if traceback_info: + print("{0}{1} test{2} failed.".format( + WARN, + len(traceback_info), + "s" if len(traceback_info) > 1 else "" + )) + for err in traceback_info: + print("\nTraceback for {0[0]}:\n{0[1]}".format(err)) + sys.exit() + print("{0[success]}SUCCESS{0}: All tests passed.".format(color)) - if not test_names: - print("\n{0}No test functions in {1[module]}{2}.py{1}".format(WARN, color, filename)) - return [], {} - if ARGV['coverage']: - ARGV['always_transact'] = True - always_transact = True +def run_test(module, network, test_names): + network.rpc.reset() + if type(CONFIG['test']['gas_limit']) is int: + network.gas_limit(CONFIG['test']['gas_limit']) traceback_info = [] if 'setup' in test_names: @@ -254,22 +205,20 @@ def run_test(filename, network, idx): default_args['skip'] == "coverage" and ARGV['coverage'] ): return [], {} - p = TestPrinter(filename, 0, len(test_names)) + p = TestPrinter(module.__file__, 0, len(test_names)) traceback_info += run_test_method(fn, default_args, p) if traceback_info: return traceback_info, {} else: - p = TestPrinter(filename, 1, len(test_names)) + p = TestPrinter(module.__file__, 1, len(test_names)) default_args = FalseyDict() network.rpc.snapshot() - for t in test_names[idx]: + for t in test_names: network.rpc.revert() fn, fn_args = _get_fn(module, t) args = default_args.copy() args.update(fn_args) traceback_info += run_test_method(fn, args, p) - if ARGV['coverage']: - ARGV['always_transact'] = always_transact if traceback_info and traceback_info[-1][2] == ReadTimeout: print(WARN+"RPC crashed, terminating test") network.rpc.kill(False) @@ -284,16 +233,6 @@ def run_test(filename, network, idx): return traceback_info, coverage_eval -def _get_fn(module, name): - fn = getattr(module, name) - if not fn.__defaults__: - return fn, FalseyDict() - return fn, FalseyDict(zip( - fn.__code__.co_varnames[:len(fn.__defaults__)], - fn.__defaults__ - )) - - def run_test_method(fn, args, p): desc = fn.__doc__ or fn.__name__ if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): @@ -326,32 +265,65 @@ def run_test_method(fn, args, p): )] -def display_report(coverage_eval): - print("\nCoverage analysis:\n") +def display_report(coverage_files, save): + coverage_eval = merge_coverage(coverage_files) + report = generate_report(coverage_eval) + print("\nCoverage analysis:") for name in coverage_eval: pct = coverage_eval[name].pop('pct') - c = next(i[1] for i in COVERAGE_COLORS if pct <= i[0]) - print(" contract: {0[contract]}{1}{0} - {2}{3:.1%}{0}".format(color, name, color(c), pct)) + c = color(next(i[1] for i in COVERAGE_COLORS if pct <= i[0])) + print("\n contract: {0[contract]}{1}{0} - {2}{3:.1%}{0}".format(color, name, c, pct)) for fn_name, pct in [(x, v[x]['pct']) for v in coverage_eval[name].values() for x in v]: - c = next(i[1] for i in COVERAGE_COLORS if pct <= i[0]) print(" {0[contract_method]}{1}{0} - {2}{3:.1%}{0}".format( - color, fn_name, color(c), pct + color, + fn_name, + color(next(i[1] for i in COVERAGE_COLORS if pct <= i[0])), + pct )) - print() + if not save: + return + filename = "coverage-"+time.strftime('%d%m%y')+"{}.json" + path = Path(CONFIG['folders']['project']).joinpath('reports') + count = len(list(path.glob(filename.format('*')))) + path = path.joinpath(filename.format("-"+str(count) if count else "")) + json.dump(report, path.open('w'), sort_keys=True, indent=2, default=sorted) + print("\nCoverage report saved at {}".format(path.relative_to(CONFIG['folders']['project']))) + + +def display_gas_profile(): + print('\nGas Profile:') + gas = transaction.gas_profile + for i in sorted(gas): + print("{0} - avg: {1[avg]:.0f} low: {1[low]} high: {1[high]}".format(i, gas[i])) + + +def _get_fn(module, name): + fn = getattr(module, name) + if not fn.__defaults__: + return fn, FalseyDict() + return fn, FalseyDict(zip( + fn.__code__.co_varnames[:len(fn.__defaults__)], + fn.__defaults__ + )) class TestPrinter: + grand_count = 1 + grand_total = 0 + def __init__(self, path, count, total): self.path = path self.count = count self.total = total self.total_time = time.time() - print("\nRunning {0[module]}{1}.py{0} - {2} test{3}".format( + print("\nRunning {0[module]}{1}{0} - {2} test{3} ({4}/{5})".format( color, path, total, - "s" if total != 1 else "" + "s" if total != 1 else "", + self.grand_count, + self.grand_total )) def skip(self, description): @@ -385,11 +357,12 @@ def stop(self, err=None, expect=False): self.count += 1 def finish(self): - print("Completed {0[module]}{1}.py{0} ({2:.4f}s)".format( + print("Completed {0[module]}{1}{0} ({2:.4f}s)".format( color, self.path, time.time() - self.total_time )) + TestPrinter.grand_count += 1 def _print(self, msg, symbol=" ", symbol_color="success", main_color=None): sys.stdout.write("\r {}{}{} {} - {}{}".format( From e49b90cc5dccbcb0bcb5047eec210daa97b13189 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 20:55:31 +0300 Subject: [PATCH 64/72] pep8 linting --- brownie/_config.py | 11 +++++----- brownie/cli/__main__.py | 1 - brownie/cli/bake.py | 3 +-- brownie/cli/compile.py | 3 +-- brownie/cli/console.py | 10 ++++----- brownie/cli/run.py | 12 ++++++----- brownie/cli/utils/__init__.py | 2 +- brownie/cli/utils/color.py | 30 +++++++++++++-------------- brownie/exceptions.py | 3 +-- brownie/network/__init__.py | 2 +- brownie/network/alert.py | 10 ++++++--- brownie/network/event.py | 5 +++-- brownie/network/history.py | 2 +- brownie/network/transaction.py | 38 ++++++++++++++++++++-------------- brownie/project/__init__.py | 4 +++- brownie/project/build.py | 2 +- brownie/test/check.py | 4 ++-- brownie/types/__init__.py | 2 +- brownie/types/convert.py | 4 ++-- 19 files changed, 81 insertions(+), 67 deletions(-) diff --git a/brownie/_config.py b/brownie/_config.py index 6d50e7142..0c55b3d7f 100644 --- a/brownie/_config.py +++ b/brownie/_config.py @@ -11,8 +11,8 @@ _Singleton ) -REPLACE_IN_UPDATE = ['active_network', 'networks'] -IGNORE_IF_MISSING = ['folders', 'logging'] +REPLACE = ['active_network', 'networks'] +IGNORE = ['folders', 'logging'] def _load_default_config(): @@ -30,7 +30,7 @@ def _load_default_config(): config['logging'].setdefault('exc', 0) for k, v in [(k, v) for k, v in config['logging'].items() if type(v) is list]: config['logging'][k] = v[1 if '--verbose' in sys.argv else 0] - except: + except Exception: config['logging'] = {"tx": 1, "exc": 1} return config @@ -71,18 +71,19 @@ def modify_network_config(network=None): # merges project .json with brownie .json def _recursive_update(original, new, base): for k in new: - if type(new[k]) is dict and k in REPLACE_IN_UPDATE: + if type(new[k]) is dict and k in REPLACE: original[k] = new[k] elif type(new[k]) is dict and k in original: _recursive_update(original[k], new[k], base+[k]) else: original[k] = new[k] - for k in [i for i in original if i not in new and not set(base+[i]).intersection(IGNORE_IF_MISSING)]: + for k in [i for i in original if i not in new and not set(base+[i]).intersection(IGNORE)]: print( "WARNING: Value '{}' not found in the config file for this project." " The default setting has been used.".format(".".join(base+[k])) ) + # move argv flags into FalseyDict ARGV = _Singleton("Argv", (FalseyDict,), {})() for key in [i for i in sys.argv if i[:2] == "--"]: diff --git a/brownie/cli/__main__.py b/brownie/cli/__main__.py index af0e55784..c8601a832 100644 --- a/brownie/cli/__main__.py +++ b/brownie/cli/__main__.py @@ -44,7 +44,6 @@ def main(): args = docopt(__doc__) sys.argv += opts - cmd_list = [i.stem for i in Path(__file__).parent.glob('[!_]*.py')] if args[''] not in cmd_list: sys.exit("Invalid command. Try 'brownie --help' for available commands.") diff --git a/brownie/cli/bake.py b/brownie/cli/bake.py index 4df7e0133..7a3ea0460 100644 --- a/brownie/cli/bake.py +++ b/brownie/cli/bake.py @@ -29,7 +29,6 @@ """ - def main(): args = docopt(__doc__) path = Path(args[''] or '.').resolve() @@ -54,4 +53,4 @@ def main(): ) print("Brownie mix '{}' has been initiated at {}".format(args[''], final_path)) - sys.exit() \ No newline at end of file + sys.exit() diff --git a/brownie/cli/compile.py b/brownie/cli/compile.py index 6d172f3b6..1a399f577 100644 --- a/brownie/cli/compile.py +++ b/brownie/cli/compile.py @@ -1,7 +1,6 @@ #!/usr/bin/python3 from docopt import docopt -from pathlib import Path import shutil import brownie.project as project @@ -18,7 +17,7 @@ def main(): - args = docopt(__doc__) + docopt(__doc__) project_path = project.check_for_project('.') build_path = project_path.joinpath('build/contracts') if ARGV['all']: diff --git a/brownie/cli/console.py b/brownie/cli/console.py index b7ba9ae1d..db7e9a1bc 100644 --- a/brownie/cli/console.py +++ b/brownie/cli/console.py @@ -7,17 +7,17 @@ import sys import threading +import brownie +import brownie.network as network +from brownie.cli.utils import color +from brownie._config import ARGV, CONFIG + if sys.platform == "win32": from pyreadline import Readline readline = Readline() else: import readline -import brownie -import brownie.network as network -from brownie.cli.utils import color -from brownie._config import ARGV, CONFIG - __doc__ = """Usage: brownie console [options] diff --git a/brownie/cli/run.py b/brownie/cli/run.py index 62d6f99eb..3f0b4e579 100644 --- a/brownie/cli/run.py +++ b/brownie/cli/run.py @@ -5,13 +5,11 @@ from pathlib import Path import sys - import brownie.network as network from brownie.cli.utils import color from brownie._config import ARGV, CONFIG - __doc__ = """Usage: brownie run [] [options] Arguments: @@ -27,6 +25,8 @@ Use run to execute scripts that deploy or interact with contracts on the network. """.format(CONFIG['network_defaults']['name']) +ERROR = "{0[error]}ERROR{0}: ".format(color) + def main(): args = docopt(__doc__) @@ -34,11 +34,13 @@ def main(): name = args[''].replace(".py", "") fn = args[''] or "main" if not Path(CONFIG['folders']['project']).joinpath('scripts/{}.py'.format(name)): - sys.exit("{0[error]}ERROR{0}: Cannot find {0[module]}scripts/{1}.py{0}".format(color, name)) + sys.exit(ERROR+"Cannot find {0[module]}scripts/{1}.py{0}".format(color, name)) network.connect(ARGV['network']) module = importlib.import_module("scripts."+name) if not hasattr(module, fn): - sys.exit("{0[error]}ERROR{0}: {0[module]}scripts/{1}.py{0} has no '{0[callable]}{2}{0}' function.".format(color, name, fn)) + sys.exit(ERROR+"{0[module]}scripts/{1}.py{0} has no '{0[callable]}{2}{0}' function.".format( + color, name, fn + )) print("Running '{0[module]}{1}{0}.{0[callable]}{2}{0}'...".format(color, name, fn)) try: getattr(module, fn)() @@ -46,6 +48,6 @@ def main(): except Exception as e: if CONFIG['logging']['exc'] >= 2: print("\n"+color.format_tb(sys.exc_info())) - print("\n{0[error]}ERROR{0}: Script '{0[module]}{1}{0}' failed from unhandled {2}: {3}".format( + print(ERROR+"Script '{0[module]}{1}{0}' failed from unhandled {2}: {3}".format( color, name, type(e).__name__, e )) diff --git a/brownie/cli/utils/__init__.py b/brownie/cli/utils/__init__.py index b681830c1..29ec2c652 100644 --- a/brownie/cli/utils/__init__.py +++ b/brownie/cli/utils/__init__.py @@ -2,4 +2,4 @@ from .color import Color -color = Color() \ No newline at end of file +color = Color() diff --git a/brownie/cli/utils/color.py b/brownie/cli/utils/color.py index 578290b86..67e9125c3 100755 --- a/brownie/cli/utils/color.py +++ b/brownie/cli/utils/color.py @@ -8,8 +8,8 @@ BASE = "\x1b[0;" MODIFIERS = { - 'bright':"1;", - 'dark':"2;" + 'bright': "1;", + 'dark': "2;" } COLORS = { @@ -31,7 +31,7 @@ class Color: - def __call__(self, color = None): + def __call__(self, color=None): if sys.platform == "win32": return "" if color in CONFIG['colors']: @@ -55,11 +55,11 @@ def __getitem__(self, color): return self(color) # format dicts for console printing - def pretty_dict(self, value, indent = 0, start=True): + def pretty_dict(self, value, indent=0, start=True): if start: sys.stdout.write(' '*indent+'{}{{'.format(self['dull'])) - indent+=4 - for c,k in enumerate(sorted(value.keys(), key= lambda k: str(k))): + indent += 4 + for c, k in enumerate(sorted(value.keys(), key=lambda k: str(k))): if c: sys.stdout.write(',') sys.stdout.write('\n'+' '*indent) @@ -69,38 +69,38 @@ def pretty_dict(self, value, indent = 0, start=True): sys.stdout.write("{0[key]}{1}{0[dull]}: ".format(self, k)) if _check_dict(value[k]): sys.stdout.write('{') - self.pretty_dict(value[k], indent,False) + self.pretty_dict(value[k], indent, False) continue if _check_list(value[k]): sys.stdout.write(str(value[k])[0]) self.pretty_list(value[k], indent, False) continue self._write(value[k]) - indent-=4 + indent -= 4 sys.stdout.write('\n'+' '*indent+'}') if start: sys.stdout.write('\n{}'.format(self)) sys.stdout.flush() # format lists for console printing - def pretty_list(self, value, indent = 0, start=True): - brackets = str(value)[0],str(value)[-1] + def pretty_list(self, value, indent=0, start=True): + brackets = str(value)[0], str(value)[-1] if start: - sys.stdout.write(' '*indent+'{}{}'.format(self['dull'],brackets[0])) + sys.stdout.write(' '*indent+'{}{}'.format(self['dull'], brackets[0])) if value and len(value) == len([i for i in value if _check_dict(i)]): # list of dicts sys.stdout.write('\n'+' '*(indent+4)+'{') - for c,i in enumerate(value): + for c, i in enumerate(value): if c: sys.stdout.write(',') self.pretty_dict(i, indent+4, False) - sys.stdout.write('\n'+ ' '*indent+brackets[1]) + sys.stdout.write('\n'+' '*indent+brackets[1]) elif ( - value and len(value)==len([i for i in value if type(i) is str]) and + value and len(value) == len([i for i in value if type(i) is str]) and set(len(i) for i in value) == {64} ): # list of bytes32 hexstrings (stack trace) - for c,i in enumerate(value): + for c, i in enumerate(value): if c: sys.stdout.write(',') sys.stdout.write('\n'+' '*(indent+4)) diff --git a/brownie/exceptions.py b/brownie/exceptions.py index 1f4c0e576..aceb1528c 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -26,6 +26,7 @@ def __init__(self, msg, cmd, proc, uri): ) ) + class RPCProcessError(_RPCBaseException): def __init__(self, cmd, proc, uri): @@ -38,7 +39,6 @@ def __init__(self, cmd, proc, uri): super().__init__("Able to launch RPC client, but unable to connect.", cmd, proc, uri) - class VirtualMachineError(Exception): '''Raised when a call to a contract causes an EVM exception. @@ -63,4 +63,3 @@ def __init__(self, exc): super().__init__(exc['message']+"\n"+exc['source']) else: super().__init__(exc['message']) - diff --git a/brownie/network/__init__.py b/brownie/network/__init__.py index bc679ed00..755d4f3e4 100644 --- a/brownie/network/__init__.py +++ b/brownie/network/__init__.py @@ -89,4 +89,4 @@ def gas_limit(*args): return "Invalid gas limit." return "Gas limit is set to {}".format( CONFIG['active_network']['gas_limit'] or "automatic" - ) \ No newline at end of file + ) diff --git a/brownie/network/alert.py b/brownie/network/alert.py index 949e4d15e..ebaf96bad 100644 --- a/brownie/network/alert.py +++ b/brownie/network/alert.py @@ -7,11 +7,12 @@ _instances = set() + class Alert: '''Setup notifications and callbacks based on state changes to the blockchain. The alert is immediatly active as soon as the class is insantiated.''' - + def __init__(self, fn, args=[], kwargs={}, delay=0.5, msg=None, callback=None): '''Creates a new Alert. @@ -32,7 +33,7 @@ def __init__(self, fn, args=[], kwargs={}, delay=0.5, msg=None, callback=None): args=(fn, args, kwargs, delay, msg, callback)) self._thread.start() _instances.add(self) - + def _loop(self, fn, args, kwargs, delay, msg, callback): start_value = fn(*args, **kwargs) while not self._kill: @@ -54,15 +55,18 @@ def stop(self): self._thread.join() _instances.discard(self) + def new(fn, args=[], kwargs={}, delay=0.5, msg=None, callback=None): '''Alias for creating a new Alert instance.''' return Alert(fn, args, kwargs, delay, msg, callback) + def show(): '''Returns a list of all currently active Alert instances.''' return list(_instances) + def stop_all(): '''Stops all currently active Alert instances.''' for t in _instances.copy(): - t.stop() \ No newline at end of file + t.stop() diff --git a/brownie/network/event.py b/brownie/network/event.py index 6745ef22f..7ba1499be 100644 --- a/brownie/network/event.py +++ b/brownie/network/event.py @@ -12,16 +12,17 @@ def _get_path(): return Path(CONFIG['folders']['brownie']).joinpath('data/topics.json') + def get_topics(abi): new_topics = _topics.copy() new_topics.update(eth_event.get_event_abi(abi)) if new_topics != _topics: _topics.update(new_topics) - json.dump( + json.dump( new_topics, _get_path().open('w'), sort_keys=True, - indent=4 + indent=2 ) return eth_event.get_topics(abi) diff --git a/brownie/network/history.py b/brownie/network/history.py index dfccc4b07..b79a0079c 100644 --- a/brownie/network/history.py +++ b/brownie/network/history.py @@ -7,9 +7,9 @@ from brownie.types.convert import to_address from .web3 import Web3 - web3 = Web3() + class TxHistory(metaclass=_Singleton): '''List-like singleton container that contains TransactionReceipt objects. diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index 24059160c..de9235ec0 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -153,17 +153,18 @@ def _await_confirm(self, silent, callback): elif CONFIG['logging']['tx']: print( ("{1} confirmed {2}- {0[key]}block{0}: {0[value]}{3}{0} " - "{0[key]}gas used{0}: {0[value]}{4}{0} ({0[value]}{5:.2%}{0})").format( - color, - self.fn_name or "Transaction", - "" if self.status else "({0[error]}{1}{0}) ".format( + "{0[key]}gas used{0}: {0[value]}{4}{0} ({0[value]}{5:.2%}{0})").format( color, - self.revert_msg or "reverted" - ), - self.block_number, - self.gas_used, - self.gas_used / self.gas_limit - )) + self.fn_name or "Transaction", + "" if self.status else "({0[error]}{1}{0}) ".format( + color, + self.revert_msg or "reverted" + ), + self.block_number, + self.gas_used, + self.gas_used / self.gas_limit + ) + ) if receipt['contractAddress']: print("{1} deployed at: {0[value]}{2}{0}".format( color, @@ -183,10 +184,15 @@ def __hash__(self): return hash(self.txid) def __getattr__(self, attr): - if attr not in ('events', 'modified_state', 'return_value', 'revert_msg', 'trace', '_trace'): - raise AttributeError( - "'TransactionReceipt' object has no attribute '{}'".format(attr) - ) + if attr not in ( + 'events', + 'modified_state', + 'return_value', + 'revert_msg', + 'trace', + '_trace' + ): + raise AttributeError("'TransactionReceipt' object has no attribute '{}'".format(attr)) if self.status == -1: return None if self._trace is None: @@ -201,7 +207,9 @@ def info(self): if self.contract_address: line = "New Contract Address{0}: {0[value]}{1}".format(color, self.contract_address) else: - line = "To{0}: {0[value]}{1.receiver}{0}\n{0[key]}Value{0}: {0[value]}{1.value}".format(color, self) + line = "To{0}: {0[value]}{1.receiver}{0}\n{0[key]}Value{0}: {0[value]}{1.value}".format( + color, self + ) if self.input != "0x00": line += "\n{0[key]}Function{0}: {0[value]}{1}".format(color, self.fn_name) print(TX_INFO.format( diff --git a/brownie/project/__init__.py b/brownie/project/__init__.py index 05ca9cb39..2dc29ed1b 100644 --- a/brownie/project/__init__.py +++ b/brownie/project/__init__.py @@ -46,7 +46,9 @@ def new(path=".", ignore_subfolder=False): if not ignore_subfolder: check = check_for_project(path) if check and check != path: - raise SystemError("Cannot make a new project inside the subfolder of an existing project.") + raise SystemError( + "Cannot make a new project inside the subfolder of an existing project." + ) for folder in [i for i in FOLDERS]: path.joinpath(folder).mkdir(exist_ok=True) if not path.joinpath('brownie-config.json').exists(): diff --git a/brownie/project/build.py b/brownie/project/build.py index 3698eaabd..44fc17996 100644 --- a/brownie/project/build.py +++ b/brownie/project/build.py @@ -120,7 +120,7 @@ def _check_coverage_hashes(self): break def __getitem__(self, contract_name): - return self._build[contract_name.replace('.json','')] + return self._build[contract_name.replace('.json', '')] def items(self): return self._build.items() diff --git a/brownie/test/check.py b/brownie/test/check.py index 287081fad..6534baf16 100644 --- a/brownie/test/check.py +++ b/brownie/test/check.py @@ -92,10 +92,10 @@ def event_fired(tx, name, count=None, values=None): return if type(values) is dict: values = [values] - if len(values) != len(events): + if len(values) != len(tx.events): raise AssertionError( "Event {} - {} events fired, {} values to match given".format( - name, len(events), len(values) + name, len(tx.events), len(values) ) ) for i in range(len(values)): diff --git a/brownie/types/__init__.py b/brownie/types/__init__.py index 937e2c175..11633cfe2 100644 --- a/brownie/types/__init__.py +++ b/brownie/types/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/python3 -from .types import * \ No newline at end of file +from .types import * # noqa: F401 F403 diff --git a/brownie/types/convert.py b/brownie/types/convert.py index 0c9ff58a7..9ba08e312 100644 --- a/brownie/types/convert.py +++ b/brownie/types/convert.py @@ -46,7 +46,7 @@ def format_input(abi, inputs): "length of {}, should be {}".format(len(inputs[i]), type_) ) inputs[i] = format_input( - {'name': name, 'inputs':[{'type': base_type}] * len(inputs[i])}, + {'name': name, 'inputs': [{'type': base_type}] * len(inputs[i])}, inputs[i] ) continue @@ -114,7 +114,7 @@ def to_address(value): def to_bytes(value, type_="bytes32"): '''Convert a value to bytes''' if type(value) not in (bytes, str, int): - raise TypeError("'{}', type {}, cannot be converted to {}".format(value, type(value), type_)) + raise TypeError("'{}', type {}, cannot convert to {}".format(value, type(value), type_)) if type_ == "byte": type_ = "bytes1" if type_ != "bytes": From cb6150f25c6b008a4969e523ec6cc540b08bd703 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 23:07:08 +0300 Subject: [PATCH 65/72] changelog, docs, bump version --- CHANGELOG | 18 +++++++++------- CONTRIBUTING.md | 1 + README.md | 4 +--- brownie/cli/__main__.py | 2 +- docs/conf.py | 2 +- docs/coverage.rst | 9 ++++---- setup.py | 46 ++++++++++++++++++++--------------------- 7 files changed, 42 insertions(+), 40 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b0385c27c..51e2b2836 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,8 +3,10 @@ - Use relative paths in build json files - Revert calls-as-transactions when evaluating coverage - - Significant refactor of coverage analysis, changes to coverageMap format - - GUI highlight reports + - Significant refactor and optimizations to coverage analysis + - changes to coverageMap format, add coverageMapTotals + - Save coverage data to reports/ subfolder + - Improvements to GUI 1.0.0b4 ------- @@ -12,12 +14,12 @@ - Add broadcast_reverting_tx flag - Use py-solc-x 0.4.0 - Detect and attach to an already active RPC client, better verbosity on RPC exceptions - - introduce Singleton metaclass and refactor code to take advantage - - add EventDict and EventItem classes for transaction event logs + - Introduce Singleton metaclass and refactor code to take advantage + - Add EventDict and EventItem classes for transaction event logs - cli.color, add _print_as_dict _print_as_list _dir_color attributes - - add conversion methods in types.convert - - remove brownie.utils package, move modules to network and project packages - - bugfixes and minor changes + - Add conversion methods in types.convert + - Remove brownie.utils package, move modules to network and project packages + - Bugfixes and minor changes 1.0.0b3 ------- @@ -103,4 +105,4 @@ 0.9.0 ----- - - Initial release \ No newline at end of file + - Initial release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9f4a906b..9307619a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,6 +10,7 @@ $ git clone https://github.com/HyperLink-Technology/brownie.git Pull requests are welcomed! Please try to adhere to the following. +- Follow PEP8 code standards (max line length 100) - include any relevant documentation updates It's a good idea to make pull requests early on. A pull request represents the diff --git a/README.md b/README.md index 4f0939827..a32a520b8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Brownie is a python framework for deploying, testing and interacting with Ethere As Brownie relies on [py-solc-x](https://github.com/iamdefinitelyahuman/py-solc-x), you do not need solc installed locally but you must install all required [solc dependencies](https://solidity.readthedocs.io/en/latest/installing-solidity.html#binary-packages). - ## Installation You can install the latest release via pip: @@ -44,9 +43,8 @@ Brownie documentation is hosted at [Read the Docs](https://eth-brownie.readthedo Help is always appreciated! In particular, Brownie needs work in the following areas before we can comfortably take it out of beta: * Tests -* Improving the documentation -* More tests * Travis or other CI +* More tests Feel free to open an issue if you find a problem, or a pull request if you've solved an issue. diff --git a/brownie/cli/__main__.py b/brownie/cli/__main__.py index c8601a832..7cf462030 100644 --- a/brownie/cli/__main__.py +++ b/brownie/cli/__main__.py @@ -9,7 +9,7 @@ from brownie.cli.utils import color import brownie.project as project -__version__ = "1.0.0b4" # did you change this in docs/conf.py as well? +__version__ = "1.0.0b5" # did you change this in docs/conf.py as well? __doc__ = """Usage: brownie [...] [options ] diff --git a/docs/conf.py b/docs/conf.py index eaa483f72..7bb7b8fca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = 'v1.0.0b4' +release = 'v1.0.0b5' # -- General configuration --------------------------------------------------- diff --git a/docs/coverage.rst b/docs/coverage.rst index 7fcb0cd48..31161bcf1 100644 --- a/docs/coverage.rst +++ b/docs/coverage.rst @@ -24,13 +24,14 @@ This will run all the test scripts in the ``tests/`` folder and give an estimate Brownie v1.0.0 - Python development framework for Ethereum Using solc version v0.5.7 + Running 4 tests across 2 modules. - Running transfer.py - 1 test + Running transfer.py - 1 test (1/2) ✓ 0 - setup (0.1882s) ✓ 1 - Transfer tokens (0.1615s) ✓ 2 - Evaluating test coverage (0.0009s) - Running approve_transferFrom.py - 3 tests + Running approve_transferFrom.py - 3 tests (2/2) ✓ 0 - setup (0.1263s) ✓ 1 - Set approval (0.2016s) ✓ 2 - Transfer tokens with transferFrom (0.1375s) @@ -39,9 +40,9 @@ This will run all the test scripts in the ``tests/`` folder and give an estimate SUCCESS: All tests passed. - Coverage analysis complete! + Coverage analysis: - contract: Token + contract: Token - 82.3% SafeMath.add - 66.7% SafeMath.sub - 100.0% Token. - 0.0% diff --git a/setup.py b/setup.py index 6c44526f7..bd60c5526 100644 --- a/setup.py +++ b/setup.py @@ -9,28 +9,28 @@ requirements = list(map(str.strip, f.read().split("\n")))[:-1] setup( - name = 'eth-brownie', + name="eth-brownie", packages=find_packages(), - version = '1.0.0b4', - license = 'MIT', - description = 'A python framework for Ethereum smart contract deployment, testing and interaction.', - long_description = long_description, - long_description_content_type = "text/markdown", - author = 'Benjamin Hauser', - author_email = 'ben.hauser@hyperlink.technology', - url = 'https://github.com/HyperLink-Technology/brownie', - keywords = ['brownie'], - install_requires = requirements, - entry_points = {"console_scripts": ["brownie=brownie.cli.__main__:main"]}, - include_package_data = True, - python_requires='>=3.6,<4', - classifiers = [ - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Build Tools', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7' + version="1.0.0b5", + license="MIT", + description="A Python framework for Ethereum smart contract deployment, testing and interaction.", # noqa: E501 + long_description=long_description, + long_description_content_type="text/markdown", + author="Benjamin Hauser", + author_email="ben.hauser@hyperlink.technology", + url="https://github.com/HyperLink-Technology/brownie", + keywords=['brownie'], + install_requires=requirements, + entry_points={'console_scripts': ["brownie=brownie.cli.__main__:main"]}, + include_package_data=True, + python_requires=">=3.6,<4", + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7" ], -) +) From 17a0e49e48ef12a562676e0bac8a82e4638c960a Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 12 May 2019 23:39:53 +0300 Subject: [PATCH 66/72] reset instead of revert when blockNumber is 0 --- brownie/network/rpc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index 0f029aa65..70181f358 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -145,7 +145,10 @@ def _revert(self, id_): id_ = self._snap() self.sleep(0) for i in self._objects: - i._revert() + if web3.eth.blockNumber == 0: + i._reset() + else: + i._revert() return id_ def _reset(self): From 8691f107f50e0f1adeb03484ff6bade419b7bf16 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 13 May 2019 01:37:08 +0300 Subject: [PATCH 67/72] evaluate coverage after each test, avoid redunant tx evaluation --- brownie/cli/test.py | 54 ++++++++++++++++++++-------------------- brownie/test/coverage.py | 30 +++++++++++++--------- 2 files changed, 45 insertions(+), 39 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 0161ad54f..cdfaa9b11 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -13,7 +13,8 @@ from brownie.cli.utils import color from brownie.test.coverage import ( analyze_coverage, - merge_coverage, + merge_coverage_eval, + merge_coverage_files, generate_report ) from brownie.exceptions import ExpectedFailing @@ -33,6 +34,8 @@ WARN = "{0[error]}WARNING{0}: ".format(color) ERROR = "{0[error]}ERROR{0}: ".format(color) +history = TxHistory() + __doc__ = """Usage: brownie test [] [] [options] Arguments: @@ -57,7 +60,6 @@ def main(): ARGV._update_from_args(args) if ARGV['coverage']: ARGV['always_transact'] = True - history = TxHistory() history._revert_lock = True test_paths = get_test_paths(args['']) @@ -178,7 +180,7 @@ def run_test_modules(test_data, save): print("\n\nTest execution has been terminated by KeyboardInterrupt.") sys.exit() finally: - print("\nTotal runtime: {:.4}s".format(time.time() - start_time)) + print("\nTotal runtime: {:.4f}s".format(time.time() - start_time)) if traceback_info: print("{0}{1} test{2} failed.".format( WARN, @@ -196,7 +198,6 @@ def run_test(module, network, test_names): if type(CONFIG['test']['gas_limit']) is int: network.gas_limit(CONFIG['test']['gas_limit']) - traceback_info = [] if 'setup' in test_names: test_names.remove('setup') fn, default_args = _get_fn(module, 'setup') @@ -206,38 +207,38 @@ def run_test(module, network, test_names): ): return [], {} p = TestPrinter(module.__file__, 0, len(test_names)) - traceback_info += run_test_method(fn, default_args, p) - if traceback_info: - return traceback_info, {} + tb, coverage_eval = run_test_method(fn, default_args, {}, p) + if tb: + return tb, {} else: p = TestPrinter(module.__file__, 1, len(test_names)) default_args = FalseyDict() + coverage_eval = {} network.rpc.snapshot() + traceback_info = [] for t in test_names: network.rpc.revert() fn, fn_args = _get_fn(module, t) args = default_args.copy() args.update(fn_args) - traceback_info += run_test_method(fn, args, p) - if traceback_info and traceback_info[-1][2] == ReadTimeout: + tb, coverage_eval = run_test_method(fn, args, coverage_eval, p) + traceback_info += tb + if tb and tb[0][2] == ReadTimeout: print(WARN+"RPC crashed, terminating test") network.rpc.kill(False) network.rpc.launch(CONFIG['active_network']['test-rpc']) break - coverage_eval = {} - if not traceback_info and ARGV['coverage']: - p.start("Evaluating test coverage") - coverage_eval = analyze_coverage(TxHistory().copy()) - p.stop() + if traceback_info and ARGV['coverage']: + coverage_eval = {} p.finish() return traceback_info, coverage_eval -def run_test_method(fn, args, p): +def run_test_method(fn, args, coverage_eval, p): desc = fn.__doc__ or fn.__name__ if args['skip'] is True or (args['skip'] == "coverage" and ARGV['coverage']): p.skip(desc) - return [] + return [], coverage_eval p.start(desc) try: if ARGV['coverage'] and 'always_transact' in args: @@ -245,28 +246,27 @@ def run_test_method(fn, args, p): fn() if ARGV['coverage']: ARGV['always_transact'] = True + coverage_eval = merge_coverage_eval( + coverage_eval, + analyze_coverage(history.copy()) + ) + history.clear() if args['pending']: raise ExpectedFailing("Test was expected to fail") p.stop() - return [] + return [], coverage_eval except Exception as e: p.stop(e, args['pending']) if type(e) != ExpectedFailing and args['pending']: - return [] + return [], coverage_eval path = Path(sys.modules[fn.__module__].__file__).relative_to(CONFIG['folders']['project']) path = "{0[module]}{1}.{0[callable]}{2}{0}".format(color, str(path)[:-3], fn.__name__) - return [( - path, - color.format_tb( - sys.exc_info(), - sys.modules[fn.__module__].__file__, - ), - type(e) - )] + tb = color.format_tb(sys.exc_info(), sys.modules[fn.__module__].__file__) + return [(path, tb, type(e))], coverage_eval def display_report(coverage_files, save): - coverage_eval = merge_coverage(coverage_files) + coverage_eval = merge_coverage_files(coverage_files) report = generate_report(coverage_eval) print("\nCoverage analysis:") for name in coverage_eval: diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 1380aea4d..2d6fd32f1 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -26,7 +26,7 @@ def analyze_coverage(history): pc = t['pc'] name = t['contractName'] path = t['source']['filename'] - if not name or not path: + if not name or not path or name not in build: continue # prevent repeated requests to build object @@ -72,16 +72,10 @@ def analyze_coverage(history): return _calculate_pct(coverage_eval) -def merge_coverage(coverage_files): - '''Given a list of coverage evaluation json file paths, returns an aggregated - coverage evaluation dict. - ''' - merged_eval = {} - for filename in coverage_files: - path = Path(filename) - if not path.exists(): - continue - coverage = json.load(path.open())['coverage'] +def merge_coverage_eval(*coverage_eval): + '''Given a list of coverage evaluation dicts, returns an aggregated evaluation dict.''' + merged_eval = coverage_eval[0] + for coverage in coverage_eval[1:]: for contract_name in list(coverage): del coverage[contract_name]['pct'] if contract_name not in merged_eval: @@ -106,6 +100,17 @@ def merge_coverage(coverage_files): return _calculate_pct(merged_eval) +def merge_coverage_files(coverage_files): + '''Given a list of coverage evaluation file paths, returns an aggregated evaluation dict.''' + coverage_eval = [] + for filename in coverage_files: + path = Path(filename) + if not path.exists(): + continue + coverage_eval.append(json.load(path.open())['coverage']) + return merge_coverage_eval(*coverage_eval) + + def _calculate_pct(coverage_eval): '''Internal method to calculate coverage percentages''' for name in coverage_eval: @@ -121,7 +126,8 @@ def _calculate_pct(coverage_eval): contract_count += build[name]['coverageMapTotals'][fn_name] result[fn_name] = {'pct': result[fn_name]['pct']} continue - result = result[fn_name] + result = dict((k, list(v) if type(v) is set else v) for k, v in result[fn_name].items()) + coverage_eval[name][path][fn_name] = result count = 0 maps = coverage_map[path][fn_name] for idx, item in enumerate(maps): From d2119367fe902cc456d970046cda00c52291092d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 13 May 2019 01:38:47 +0300 Subject: [PATCH 68/72] return error message when RPC call fails --- brownie/exceptions.py | 4 ++++ brownie/network/rpc.py | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/brownie/exceptions.py b/brownie/exceptions.py index aceb1528c..0ec455cf5 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -39,6 +39,10 @@ def __init__(self, cmd, proc, uri): super().__init__("Able to launch RPC client, but unable to connect.", cmd, proc, uri) +class RPCRequestError(Exception): + pass + + class VirtualMachineError(Exception): '''Raised when a call to a contract causes an EVM exception. diff --git a/brownie/network/rpc.py b/brownie/network/rpc.py index 70181f358..f40e36000 100644 --- a/brownie/network/rpc.py +++ b/brownie/network/rpc.py @@ -9,7 +9,7 @@ from .web3 import Web3 from brownie.types.types import _Singleton -from brownie.exceptions import RPCProcessError, RPCConnectionError +from brownie.exceptions import RPCProcessError, RPCConnectionError, RPCRequestError web3 = Web3() @@ -131,9 +131,12 @@ def _request(self, *args): if not self.is_active(): raise SystemError("RPC is not active.") try: - return web3.providers[0].make_request(*args)['result'] + response = web3.providers[0].make_request(*args) + if 'result' in response: + return response['result'] except IndexError: raise RPCConnectionError("Web3 is not connected.") + raise RPCRequestError(response['error']['message']) def _snap(self): return self._request("evm_snapshot", []) @@ -204,12 +207,14 @@ def revert(self): '''Reverts the EVM to the most recently taken snapshot.''' if not self._snapshot_id: raise ValueError("No snapshot set") + self._internal_id = None self._snapshot_id = self._revert(self._snapshot_id) return "Block height reverted to {}".format(web3.eth.blockNumber) def reset(self): '''Reverts the EVM to the genesis state.''' self._snapshot_id = None + self._internal_id = None self._reset_id = self._revert(self._reset_id) return "Block height reset to 0" From 16e66d1c0c865407540170ed24164bc74beb1d32 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 13 May 2019 01:39:16 +0300 Subject: [PATCH 69/72] bugfixes --- brownie/cli/test.py | 9 ++++++--- brownie/network/contract.py | 9 ++++++--- brownie/network/history.py | 3 +++ brownie/project/build.py | 9 ++++++--- brownie/project/sources.py | 1 + brownie/test/coverage.py | 2 +- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index cdfaa9b11..3afc7b0bf 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -111,8 +111,11 @@ def get_test_data(test_paths): coverage_files = [] test_data = [] project = Path(CONFIG['folders']['project']) + build_path = project.joinpath('build/coverage') for path in test_paths: - coverage_json = project.joinpath("build/coverage/"+path.stem+".json") + path = path.relative_to(project) + coverage_json = build_path.joinpath(path.parent.relative_to('tests')) + coverage_json = coverage_json.joinpath(path.stem+".json") coverage_files.append(coverage_json) if coverage_json.exists(): coverage_eval = json.load(coverage_json.open())['coverage'] @@ -122,7 +125,7 @@ def get_test_data(test_paths): coverage_eval = {} for p in list(coverage_json.parents)[::-1]: p.mkdir(exist_ok=True) - module_name = str(path.relative_to(project))[:-3].replace(os.sep, '.') + module_name = str(path)[:-3].replace(os.sep, '.') module = importlib.import_module(module_name) test_names = re.findall(r'\ndef[\s ]{1,}([^_]\w*)[\s ]*\([^)]*\)', path.open().read()) if not test_names: @@ -131,7 +134,7 @@ def get_test_data(test_paths): duplicates = set([i for i in test_names if test_names.count(i) > 1]) if duplicates: raise ValueError("{} contains multiple test methods of the same name: {}".format( - path.relative_to(project), + path, ", ".join(duplicates) )) if 'setup' in test_names: diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 8bfaca0d6..600663348 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -284,7 +284,7 @@ def call(self, *args): return format_output(result[0]) return KwargTuple(result, self.abi) - def transact(self, *args): + def transact(self, *args, _rpc_clear=True): '''Broadcasts a transaction that calls this contract method. Args: @@ -299,7 +299,8 @@ def transact(self, *args): "Contract has no owner, you must supply a tx dict" " with a 'from' field as the last argument." ) - rpc._internal_clear() + if _rpc_clear: + rpc._internal_clear() return tx['from'].transfer( self._address, tx['value'], @@ -368,7 +369,9 @@ def __call__(self, *args): Contract method return value(s).''' if ARGV['always_transact']: rpc._internal_snap() - tx = self.transact(*args, {'gas_price': 0}) + args, tx = _get_tx(self._owner, args) + tx['gas_price'] = 0 + tx = self.transact(*args, tx, _rpc_clear=False) if tx.modified_state: rpc._internal_revert() return tx.return_value diff --git a/brownie/network/history.py b/brownie/network/history.py index b79a0079c..1d8533cd2 100644 --- a/brownie/network/history.py +++ b/brownie/network/history.py @@ -51,6 +51,9 @@ def _console_repr(self): def _add_tx(self, tx): self._list.append(tx) + def clear(self): + self._list.clear() + def copy(self): '''Returns a shallow copy of the object as a list''' return self._list.copy() diff --git a/brownie/project/build.py b/brownie/project/build.py index 44fc17996..b09149329 100644 --- a/brownie/project/build.py +++ b/brownie/project/build.py @@ -47,6 +47,12 @@ def __init__(self): self._build = {} self._path = None + def __getitem__(self, contract_name): + return self._build[contract_name.replace('.json', '')] + + def __contains__(self, contract_name): + return contract_name.replace('.json', '') in self._build + def _load(self): self. _path = Path(CONFIG['folders']['project']).joinpath('build/contracts') # check build paths @@ -119,9 +125,6 @@ def _check_coverage_hashes(self): coverage_json.unlink() break - def __getitem__(self, contract_name): - return self._build[contract_name.replace('.json', '')] - def items(self): return self._build.items() diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 63d1ec9a7..03846e9dc 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -134,6 +134,7 @@ def inheritance_map(self): def add_source(self, source): path = "".format(self._string_iter) self._source[path] = source + self._remove_comments(path) self._get_contract_data(path) self._string_iter += 1 return path diff --git a/brownie/test/coverage.py b/brownie/test/coverage.py index 2d6fd32f1..496b7f8c9 100644 --- a/brownie/test/coverage.py +++ b/brownie/test/coverage.py @@ -208,7 +208,7 @@ def _evaluate_branch(path, ln): after = next((i for i in after if i != ")"), after[0])[0] if ( (before[-2:] == "if" and after == "|") or - (before[:7] == "require" and after in (")", "|")) + (before[:7] == "require" and after in (")", "|", ",")) ): return True return False From ee50389dc01f3feb97897edba3c6b3c2ec4efc15 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 13 May 2019 03:51:01 +0300 Subject: [PATCH 70/72] add broadcast_reverting_tx test value override, add warning, update docs --- brownie/cli/test.py | 8 ++++++-- brownie/data/config.json | 3 ++- brownie/network/contract.py | 2 +- docs/config.rst | 6 ++++-- docs/tests.rst | 9 +++++++++ 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 3afc7b0bf..4523a32d3 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -58,6 +58,8 @@ def main(): args = docopt(__doc__) ARGV._update_from_args(args) +# if type(CONFIG['test']['gas_limit']) is int: +# network.gas_limit(CONFIG['test']['gas_limit']) if ARGV['coverage']: ARGV['always_transact'] = True history._revert_lock = True @@ -150,6 +152,10 @@ def run_test_modules(test_data, save): count = sum([len([x for x in i[3] if x != "setup"]) for i in test_data]) print("Running {} tests across {} modules.".format(count, len(test_data))) network.connect(ARGV['network']) + for key in ('broadcast_reverting_tx', 'gas_limit'): + CONFIG['active_network'][key] = CONFIG['test'][key] + if not CONFIG['active_network']['broadcast_reverting_tx']: + print("{0[error]}WARNING{0}: Reverting transactions will NOT be broadcasted.".format(color)) traceback_info = [] start_time = time.time() try: @@ -198,8 +204,6 @@ def run_test_modules(test_data, save): def run_test(module, network, test_names): network.rpc.reset() - if type(CONFIG['test']['gas_limit']) is int: - network.gas_limit(CONFIG['test']['gas_limit']) if 'setup' in test_names: test_names.remove('setup') diff --git a/brownie/data/config.json b/brownie/data/config.json index 9fe343c91..24e24528f 100644 --- a/brownie/data/config.json +++ b/brownie/data/config.json @@ -17,7 +17,8 @@ }, "test":{ "gas_limit": 6721975, - "default_contract_owner": false + "default_contract_owner": false, + "broadcast_reverting_tx": true }, "solc":{ "optimize": true, diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 600663348..481449b49 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -332,7 +332,7 @@ class ContractTx(_ContractMethod): def __init__(self, fn, abi, name, owner): if ( - ARGV['cli'] != "console" and not + ARGV['cli'] == "test" and not CONFIG['test']['default_contract_owner'] ): owner = None diff --git a/docs/config.rst b/docs/config.rst index 42c2fe0bf..918a0b713 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -43,9 +43,11 @@ The following settings are available: .. py:attribute:: test - Properties that affect only affect Brownie's configuration when running scripts and tests. See Test :ref:`test_settings` for detailed information on the effects and implications of these settings. + Properties that affect only affect Brownie's configuration when running tests. See Test :ref:`test_settings` for detailed information on the effects and implications of these settings. - * ``gas_limit``: If set to an integer, this value will over-ride the default gas limit setting when running tests. + * ``gas_limit``: Replaces the default network gas limit. + + * ``broadcast_reverting_tx``: Replaces the default network setting for broadcasting reverting transactions. * ``default_contract_owner``: If ``false``, deployed contracts will not remember the account that they were created by and you will have to supply a ``from`` kwarg for every contract transaction. diff --git a/docs/tests.rst b/docs/tests.rst index 682334ecc..ff8281fd7 100644 --- a/docs/tests.rst +++ b/docs/tests.rst @@ -192,10 +192,19 @@ The following test configuration settings are available in ``brownie-config.json { "test": { "gas_limit": 6721975, + "broadcast_reverting_tx": true, "default_contract_owner": false } } +.. py:attribute:: gas_limit + + Replaces the default network gas limit. + +.. py:attribute:: broadcast_reverting_tx + + Replaces the default network setting for broadcasting transactions that would revert. + .. py:attribute:: default_contract_owner If ``True``, calls to contract transactions that do not specify a sender are broadcast from the same address that deployed the contract. From 5053bcf4a82df3c1c57df3933abfdfd8617b9af6 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 13 May 2019 04:24:19 +0300 Subject: [PATCH 71/72] sort coverage report output by contract --- brownie/cli/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brownie/cli/test.py b/brownie/cli/test.py index 4523a32d3..650b02ffe 100644 --- a/brownie/cli/test.py +++ b/brownie/cli/test.py @@ -276,7 +276,7 @@ def display_report(coverage_files, save): coverage_eval = merge_coverage_files(coverage_files) report = generate_report(coverage_eval) print("\nCoverage analysis:") - for name in coverage_eval: + for name in sorted(coverage_eval): pct = coverage_eval[name].pop('pct') c = color(next(i[1] for i in COVERAGE_COLORS if pct <= i[0])) print("\n contract: {0[contract]}{1}{0} - {2}{3:.1%}{0}".format(color, name, c, pct)) From a2467e3fc48c0a56a8345b7808a7bc63118e589b Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 13 May 2019 04:26:07 +0300 Subject: [PATCH 72/72] raise ValueError on unknown function --- brownie/project/sources.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 03846e9dc..8f60cc70b 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -121,12 +121,15 @@ def get_fn(self, name, start, stop): return False if stop > offset[2] else offset[0] def get_fn_offset(self, name, fn_name): - if name not in self._data: - name = next( - k for k, v in self._data.items() if v['sourcePath'] == str(name) and - fn_name in [i[0] for i in v['fn_offsets']] - ) - return next(i for i in self._data[name]['fn_offsets'] if i[0] == fn_name)[1:3] + try: + if name not in self._data: + name = next( + k for k, v in self._data.items() if v['sourcePath'] == str(name) and + fn_name in [i[0] for i in v['fn_offsets']] + ) + return next(i for i in self._data[name]['fn_offsets'] if i[0] == fn_name)[1:3] + except StopIteration: + raise ValueError("Unknown function '{}' in contract {}".format(fn_name, name)) def inheritance_map(self): return dict((k, v['inherited'].copy()) for k, v in self._data.items())