From 3a11cae3c7e5e7d7d201b18662e799c3196f4aee Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Thu, 19 Dec 2019 20:44:55 +0400 Subject: [PATCH 01/39] include vyper contracts in project --- brownie/project/main.py | 4 ++-- brownie/project/sources.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/brownie/project/main.py b/brownie/project/main.py index 639a4e5ea..ba5183209 100644 --- a/brownie/project/main.py +++ b/brownie/project/main.py @@ -113,8 +113,8 @@ class Project(_ProjectBase): def __init__(self, name: str, project_path: Path) -> None: contract_sources: Dict = {} - for path in project_path.glob("contracts/**/*.sol"): - if "/_" in path.as_posix(): + for path in project_path.glob("contracts/**/*"): + if "/_" in path.as_posix() or path.suffix not in (".sol", ".vy"): continue with path.open() as fp: source = fp.read() diff --git a/brownie/project/sources.py b/brownie/project/sources.py index af486b2ad..c166d1355 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -26,7 +26,11 @@ def __init__(self, contract_sources: Dict) -> None: self._source: Dict = {} self._contracts: Dict = {} for path, source in contract_sources.items(): - data = _get_contract_data(source) + if path.endswith(".vy"): + # TODO minify vyper contracts + data = {path: {"offsets": [(0, 0)], "offset_map": [(0, 0)]}} + else: + data = _get_contract_data(source) for name, values in data.items(): if name in self._contracts: raise NamespaceCollision(f"Project has multiple contracts named '{name}'") From 08ea6d010b88efe2edc73f20c969b02f9b6652b4 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 20 Dec 2019 01:39:08 +0400 Subject: [PATCH 02/39] compile vyper files (basic implementation) --- brownie/project/compiler.py | 196 ++++++++++++++++++++++++++---------- requirements.txt | 1 + setup.cfg | 2 +- 3 files changed, 146 insertions(+), 53 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 2069614b2..bd884c1a0 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -9,9 +9,11 @@ import solcast import solcx +import vyper from requests.exceptions import ConnectionError from semantic_version import NpmSpec, Version from solcast.nodes import NodeBase +from vyper.cli import vyper_json from brownie.exceptions import CompilerError, IncompatibleSolcVersion, PragmaError @@ -24,12 +26,12 @@ sh.setFormatter(logging.Formatter("%(message)s")) solcx_logger.addHandler(sh) -STANDARD_JSON = { - "language": "Solidity", +STANDARD_JSON: Dict = { + "language": None, "sources": {}, "settings": { "outputSelection": { - "*": {"*": ["abi", "evm.assembly", "evm.bytecode", "evm.deployedBytecode"], "": ["ast"]} + "*": {"*": ["abi", "evm.bytecode", "evm.deployedBytecode"], "": ["ast"]} }, "optimizer": {"enabled": True, "runs": 200}, "evmVersion": None, @@ -91,18 +93,26 @@ def compile_and_format( if not contract_sources: return {} - if solc_version is not None: + if list(contract_sources)[0].endswith(".vy"): + path_versions = {"vyper": list(contract_sources)} + elif solc_version is not None: path_versions = {solc_version: list(contract_sources)} else: path_versions = find_solc_versions(contract_sources, install_needed=True, silent=silent) build_json: Dict = {} for version, path_list in path_versions.items(): - set_solc_version(version) - compiler_data = {"minify_source": minify, "version": str(solcx.get_solc_version())} + if version == "vyper": + language = "Vyper" + compiler_data = {"minify_source": False, "version": vyper.__version__} + else: + set_solc_version(version) + language = "Solidity" + compiler_data = {"minify_source": minify, "version": str(solcx.get_solc_version())} + to_compile = dict((k, v) for k, v in contract_sources.items() if k in path_list) - input_json = generate_input_json(to_compile, optimize, runs, evm_version, minify) + input_json = generate_input_json(to_compile, optimize, runs, evm_version, minify, language) output_json = compile_from_input_json(input_json, silent, allow_paths) build_json.update(generate_build_json(input_json, output_json, compiler_data, silent)) return build_json @@ -247,6 +257,7 @@ def generate_input_json( runs: int = 200, evm_version: Union[int, str, None] = None, minify: bool = False, + language: str = "Solidity", ) -> Dict: """Formats contracts to the standard solc input json. @@ -259,12 +270,20 @@ def generate_input_json( Returns: dict """ + if evm_version is None: - evm_version = next(i[0] for i in EVM_SOLC_VERSIONS if solcx.get_solc_version() >= i[1]) + if language == "Solidity": + evm_version = next(i[0] for i in EVM_SOLC_VERSIONS if solcx.get_solc_version() >= i[1]) + else: + evm_version = "byzantium" + input_json: Dict = deepcopy(STANDARD_JSON) + input_json["language"] = language input_json["settings"]["optimizer"]["enabled"] = optimize input_json["settings"]["optimizer"]["runs"] = runs if optimize else 0 input_json["settings"]["evmVersion"] = evm_version + if language == "Vyper": + input_json["outputSelection"] = input_json["settings"].pop("outputSelection") input_json["sources"] = dict( (k, {"content": sources.minify(v)[0] if minify else v}) for k, v in contract_sources.items() ) @@ -288,15 +307,24 @@ def compile_from_input_json( optimizer = input_json["settings"]["optimizer"] input_json["settings"].setdefault("evmVersion", None) + if not silent: print("Compiling contracts...") - print(f" Solc version: {str(solcx.get_solc_version())}") - print( - " Optimizer: " - + (f"Enabled Runs: {optimizer['runs']}" if optimizer["enabled"] else "Disabled") - ) + if input_json["language"] == "Vyper": + print(f" Vyper version: {vyper.__version__}") + else: + print(f" Solc version: {str(solcx.get_solc_version())}") + + print( + " Optimizer: " + + (f"Enabled Runs: {optimizer['runs']}" if optimizer["enabled"] else "Disabled") + ) if input_json["settings"]["evmVersion"]: print(f" EVM Version: {input_json['settings']['evmVersion'].capitalize()}") + + if input_json["language"] == "Vyper": + return vyper_json.compile_json(input_json, root_path=allow_paths) + try: return solcx.compile_standard( input_json, @@ -337,9 +365,10 @@ def generate_build_json( build_json = {} path_list = list(input_json["sources"]) - source_nodes = solcast.from_standard_output(output_json) - statement_nodes = _get_statement_nodes(source_nodes) - branch_nodes = _get_branch_nodes(source_nodes) + if input_json["language"] == "Solidity": + source_nodes = solcast.from_standard_output(output_json) + statement_nodes = _get_statement_nodes(source_nodes) + branch_nodes = _get_branch_nodes(source_nodes) for path, contract_name in [(k, v) for k in path_list for v in output_json["contracts"][k]]: @@ -350,42 +379,63 @@ def generate_build_json( evm = output_json["contracts"][path][contract_name]["evm"] bytecode = _format_link_references(evm) hash_ = sources.get_hash(input_json["sources"][path]["content"], contract_name, minified) - node = next(i[contract_name] for i in source_nodes if i.absolutePath == path) - paths = sorted( - set([node.parent().absolutePath] + [i.parent().absolutePath for i in node.dependencies]) - ) - pc_map, statement_map, branch_map = _generate_coverage_data( - evm["deployedBytecode"]["sourceMap"], - evm["deployedBytecode"]["opcodes"], - node, - statement_nodes, - branch_nodes, - next((True for i in abi if i["type"] == "fallback"), False), - ) + if input_json["language"] == "Solidity": + node = next(i[contract_name] for i in source_nodes if i.absolutePath == path) + paths = sorted( + set( + [node.parent().absolutePath] + + [i.parent().absolutePath for i in node.dependencies] + ) + ) + pc_map, statement_map, branch_map = _generate_solc_coverage_data( + evm["deployedBytecode"]["sourceMap"], + evm["deployedBytecode"]["opcodes"], + node, + statement_nodes, + branch_nodes, + next((True for i in abi if i["type"] == "fallback"), False), + ) + build_json[contract_name] = { + "allSourcePaths": paths, + "coverageMap": {"statements": statement_map, "branches": branch_map}, + "dependencies": [i.name for i in node.dependencies], + "offset": node.offset, + "pcMap": pc_map, + "type": node.contractKind, + } - build_json[contract_name] = { - "abi": abi, - "allSourcePaths": paths, - "ast": output_json["sources"][path]["ast"], - "bytecode": bytecode, - "bytecodeSha1": _get_bytecode_hash(bytecode), - "compiler": compiler_data, - "contractName": contract_name, - "coverageMap": {"statements": statement_map, "branches": branch_map}, - "deployedBytecode": evm["deployedBytecode"]["object"], - "deployedSourceMap": evm["deployedBytecode"]["sourceMap"], - "dependencies": [i.name for i in node.dependencies], - # 'networks': {}, - "offset": node.offset, - "opcodes": evm["deployedBytecode"]["opcodes"], - "pcMap": pc_map, - "sha1": hash_, - "source": input_json["sources"][path]["content"], - "sourceMap": evm["bytecode"]["sourceMap"], - "sourcePath": path, - "type": node.contractKind, - } + else: + pc_map = _generate_vyper_coverage_data( + evm["deployedBytecode"]["sourceMap"], evm["deployedBytecode"]["opcodes"], path + ) + build_json[contract_name] = { + "allSourcePaths": [path], + "coverageMap": {"statements": {}, "branches": {}}, + "dependencies": [], + "offset": [0, -1], + "pcMap": pc_map, + "type": "contract", + } + + build_json[contract_name].update( + { + "abi": abi, + "ast": output_json["sources"][path]["ast"], + "bytecode": bytecode, + "bytecodeSha1": _get_bytecode_hash(bytecode), + "compiler": compiler_data, + "contractName": contract_name, + "deployedBytecode": evm["deployedBytecode"]["object"], + "deployedSourceMap": evm["deployedBytecode"]["sourceMap"], + # 'networks': {}, + "opcodes": evm["deployedBytecode"]["opcodes"], + "sha1": hash_, + "source": input_json["sources"][path]["content"], + "sourceMap": evm["bytecode"].get("sourceMap", ""), + "sourcePath": path, + } + ) if not silent: print("") @@ -396,7 +446,9 @@ def generate_build_json( def _format_link_references(evm: Dict) -> Dict: # Standardizes formatting for unlinked libraries within bytecode bytecode = evm["bytecode"]["object"] - references = [(k, x) for v in evm["bytecode"]["linkReferences"].values() for k, x in v.items()] + references = [ + (k, x) for v in evm["bytecode"].get("linkReferences", {}).values() for k, x in v.items() + ] for n, loc in [(i[0], x["start"] * 2) for i in references for x in i[1]]: bytecode = f"{bytecode[:loc]}__{n[:36]:_<36}__{bytecode[loc+40:]}" return bytecode @@ -407,7 +459,7 @@ def _get_bytecode_hash(bytecode: Dict) -> str: return sha1(bytecode[:-68].encode()).hexdigest() -def _generate_coverage_data( +def _generate_solc_coverage_data( source_map_str: str, opcodes_str: str, contract_node: Any, @@ -699,3 +751,43 @@ def _check_left_operator(node: NodeBase, depth: int) -> bool: i for i in parents if i.leftExpression == node or node.is_child_of(i.leftExpression) ).operator return op == "||" + + +def _generate_vyper_coverage_data(source_map_str: str, opcodes_str: str, source_path: str): + if not opcodes_str: + return {} + + source_map = deque(_expand_source_map(source_map_str)) + opcodes = deque(opcodes_str.split(" ")) + + pc_list: List = [] + pc = 0 + + while opcodes: + + # format of source is [start, stop, contract_id, jump code] + source = source_map.popleft() + pc_list.append({"op": opcodes.popleft(), "pc": pc, "path": source_path}) + + if source[3] != "-": + pc_list[-1]["jump"] = source[3] + + pc += 1 + if opcodes and opcodes[0][:2] == "0x": + pc_list[-1]["value"] = opcodes.popleft() + pc += int(pc_list[-1]["op"][4:]) + + # set source offset (-1 means none) + if source[0] == -1: + continue + offset = (source[0], source[0] + source[1]) + pc_list[-1]["offset"] = offset + + # TODO invalid reason + # TODO nonpayable + # TODO fn name + # TODO dev revert strings + + pc_map = dict((i.pop("pc"), i) for i in pc_list) + + return pc_map diff --git a/requirements.txt b/requirements.txt index 28681e786..1ff3932da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ pythx==1.4.1 pyyaml>=5.1.0,<6.0.0 requests>=2.22.0,<3.0.0 semantic-version>=2.8.2,<3.0.0 +vyper==0.1.0b14 web3==5.3.0 diff --git a/setup.cfg b/setup.cfg index e230e3a05..13fbb2880 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ ignore = E203,W503 force_grid_wrap = 0 include_trailing_comma = True known_first_party = brownie -known_third_party = _pytest,docopt,ens,eth_abi,eth_event,eth_hash,eth_keys,eth_utils,ethpm,hexbytes,mythx_models,psutil,pytest,pythx,requests,semantic_version,setuptools,solcast,solcx,web3,yaml +known_third_party = _pytest,docopt,ens,eth_abi,eth_event,eth_hash,eth_keys,eth_utils,ethpm,hexbytes,mythx_models,psutil,pytest,pythx,requests,semantic_version,setuptools,solcast,solcx,vyper,web3,yaml line_length = 100 multi_line_output = 3 use_parentheses = True From 0a3ba740c94e15a3da1cb218ec5e3b2cab358507 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 20 Dec 2019 02:01:56 +0400 Subject: [PATCH 03/39] use constant field in abi for setting function type --- brownie/network/contract.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 211eedbdd..3cbd6f2da 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -515,7 +515,13 @@ def _get_tx(owner: Optional[AccountsType], args: Tuple) -> Tuple: def _get_method_object( address: str, abi: Dict, name: str, owner: Optional[AccountsType] ) -> Union["ContractCall", "ContractTx"]: - if abi["stateMutability"] in ("view", "pure"): + + if "constant" in abi: + constant = abi["constant"] + else: + constant = abi["stateMutability"] in ("view", "pure") + + if constant: return ContractCall(address, abi, name, owner) return ContractTx(address, abi, name, owner) From b87c8d3ac8b1c5a1127428dcf9e47e43752e2fd3 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 20 Dec 2019 16:41:05 +0400 Subject: [PATCH 04/39] include vyper function names in pcMap --- brownie/project/compiler.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index bd884c1a0..26c75cf9c 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -12,7 +12,7 @@ import vyper from requests.exceptions import ConnectionError from semantic_version import NpmSpec, Version -from solcast.nodes import NodeBase +from solcast.nodes import NodeBase, is_inside_offset from vyper.cli import vyper_json from brownie.exceptions import CompilerError, IncompatibleSolcVersion, PragmaError @@ -407,13 +407,17 @@ def generate_build_json( else: pc_map = _generate_vyper_coverage_data( - evm["deployedBytecode"]["sourceMap"], evm["deployedBytecode"]["opcodes"], path + evm["deployedBytecode"]["sourceMap"], + evm["deployedBytecode"]["opcodes"], + path, + contract_name, + output_json["sources"][path]["ast"], ) build_json[contract_name] = { "allSourcePaths": [path], "coverageMap": {"statements": {}, "branches": {}}, "dependencies": [], - "offset": [0, -1], + "offset": [0, len(input_json["sources"][path]["content"])], "pcMap": pc_map, "type": "contract", } @@ -753,12 +757,22 @@ def _check_left_operator(node: NodeBase, depth: int) -> bool: return op == "||" -def _generate_vyper_coverage_data(source_map_str: str, opcodes_str: str, source_path: str): +def _convert_src(src): + src = [int(i) for i in src.split(":")[:2]] + return src[0], src[0] + src[1] + + +def _generate_vyper_coverage_data( + source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: Dict +): if not opcodes_str: return {} source_map = deque(_expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) + fn_offsets = dict( + (i["name"], _convert_src(i["src"])) for i in ast_json if i["ast_type"] == "FunctionDef" + ) pc_list: List = [] pc = 0 @@ -767,7 +781,7 @@ def _generate_vyper_coverage_data(source_map_str: str, opcodes_str: str, source_ # format of source is [start, stop, contract_id, jump code] source = source_map.popleft() - pc_list.append({"op": opcodes.popleft(), "pc": pc, "path": source_path}) + pc_list.append({"op": opcodes.popleft(), "pc": pc}) if source[3] != "-": pc_list[-1]["jump"] = source[3] @@ -781,13 +795,19 @@ def _generate_vyper_coverage_data(source_map_str: str, opcodes_str: str, source_ if source[0] == -1: continue offset = (source[0], source[0] + source[1]) + pc_list[-1]["path"] = source_str pc_list[-1]["offset"] = offset + fn = next((k for k, v in fn_offsets.items() if is_inside_offset(offset, v)), None) + if fn: + pc_list[-1]["fn"] = f"{contract_name}.{fn}" + # TODO invalid reason # TODO nonpayable - # TODO fn name # TODO dev revert strings + pc_list[0]["path"] = source_str + pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] pc_map = dict((i.pop("pc"), i) for i in pc_list) return pc_map From 4f753c0fe67e1069b92d617b5488f0bad9809267 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 20 Dec 2019 16:41:33 +0400 Subject: [PATCH 05/39] properly display vy files in gui --- brownie/_gui/source.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/brownie/_gui/source.py b/brownie/_gui/source.py index 68cb80e48..fe1957813 100755 --- a/brownie/_gui/source.py +++ b/brownie/_gui/source.py @@ -23,8 +23,9 @@ def __init__(self, parent): self.root.bind("", self.key_left) self.root.bind("", self.key_right) base_path = self.root.active_project._path.joinpath("contracts") - for path in base_path.glob("**/*.sol"): - self.add(path) + for path in base_path.glob("**/*"): + if path.suffix in (".sol", ".vy"): + self.add(path) self.set_visible([]) def add(self, path): @@ -33,7 +34,7 @@ def add(self, path): if label in [i._label for i in self._frames]: return with path.open() as fp: - frame = SourceFrame(self, fp.read()) + frame = SourceFrame(self, fp.read(), path.suffix) super().add(frame, text=f" {label} ") frame._id = len(self._frames) frame._label = label @@ -168,7 +169,7 @@ def key(k): class SourceFrame(tk.Frame): - def __init__(self, root, text): + def __init__(self, root, text, suffix): super().__init__(root) self._text = tk.Text(self, width=90, yscrollcommand=self._text_scroll) self._scroll = ttk.Scrollbar(self) @@ -183,7 +184,10 @@ def __init__(self, root, text): for k, v in TEXT_COLORS.items(): self._text.tag_config(k, **v) - pattern = r"((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + if suffix == ".sol": + pattern = r"((?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/))" + else: + pattern = r"((#[^\n]*\n)|(\"\"\"[\s\S]*?\"\"\")|('''[\s\S]*?'''))" for match in re.finditer(pattern, text): self.tag_add("comment", match.start(), match.end()) From 8c0871c07905cfcfcdab74db15273212cc163b35 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 21 Dec 2019 14:59:11 +0400 Subject: [PATCH 06/39] fix failing test by adding language field --- tests/project/test_compiler.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/project/test_compiler.py b/tests/project/test_compiler.py index be2207b59..b8600eaca 100644 --- a/tests/project/test_compiler.py +++ b/tests/project/test_compiler.py @@ -91,12 +91,18 @@ def _test_compiler(a, **kwargs): assert kwargs["optimize_runs"] == 666 monkeypatch.setattr("solcx.compile_standard", _test_compiler) - input_json = {"settings": {"optimizer": {"enabled": True, "runs": 666}}} + input_json = {"language": "Solidity", "settings": {"optimizer": {"enabled": True, "runs": 666}}} compiler.compile_from_input_json(input_json) - input_json = {"settings": {"optimizer": {"enabled": True, "runs": 31337}}} + input_json = { + "language": "Solidity", + "settings": {"optimizer": {"enabled": True, "runs": 31337}}, + } with pytest.raises(AssertionError): compiler.compile_from_input_json(input_json) - input_json = {"settings": {"optimizer": {"enabled": False, "runs": 666}}} + input_json = { + "language": "Solidity", + "settings": {"optimizer": {"enabled": False, "runs": 666}}, + } with pytest.raises(AssertionError): compiler.compile_from_input_json(input_json) From 1eea01e7e8c770c07908412b2752932570f69f15 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 21 Dec 2019 14:59:37 +0400 Subject: [PATCH 07/39] add vyper statement coverage for functions --- brownie/project/compiler.py | 59 +++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 26c75cf9c..acd0578ec 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -12,7 +12,7 @@ import vyper from requests.exceptions import ConnectionError from semantic_version import NpmSpec, Version -from solcast.nodes import NodeBase, is_inside_offset +from solcast.nodes import NodeBase from vyper.cli import vyper_json from brownie.exceptions import CompilerError, IncompatibleSolcVersion, PragmaError @@ -398,15 +398,13 @@ def generate_build_json( ) build_json[contract_name] = { "allSourcePaths": paths, - "coverageMap": {"statements": statement_map, "branches": branch_map}, "dependencies": [i.name for i in node.dependencies], "offset": node.offset, - "pcMap": pc_map, "type": node.contractKind, } else: - pc_map = _generate_vyper_coverage_data( + pc_map, statement_map, branch_map = _generate_vyper_coverage_data( evm["deployedBytecode"]["sourceMap"], evm["deployedBytecode"]["opcodes"], path, @@ -415,10 +413,8 @@ def generate_build_json( ) build_json[contract_name] = { "allSourcePaths": [path], - "coverageMap": {"statements": {}, "branches": {}}, "dependencies": [], "offset": [0, len(input_json["sources"][path]["content"])], - "pcMap": pc_map, "type": "contract", } @@ -430,10 +426,11 @@ def generate_build_json( "bytecodeSha1": _get_bytecode_hash(bytecode), "compiler": compiler_data, "contractName": contract_name, + "coverageMap": {"statements": statement_map, "branches": branch_map}, "deployedBytecode": evm["deployedBytecode"]["object"], "deployedSourceMap": evm["deployedBytecode"]["sourceMap"], - # 'networks': {}, "opcodes": evm["deployedBytecode"]["opcodes"], + "pcMap": pc_map, "sha1": hash_, "source": input_json["sources"][path]["content"], "sourceMap": evm["bytecode"].get("sourceMap", ""), @@ -569,9 +566,11 @@ def _generate_solc_coverage_data( pc_list[-1]["fn"] = pc_list[-2]["fn"] else: pc_list[-1]["fn"] = _get_fn_full_name(source_nodes[source[2]], offset) - statement = next(i for i in stmt_nodes[path] if sources.is_inside_offset(offset, i)) - stmt_nodes[path].discard(statement) - statement_map[path].setdefault(pc_list[-1]["fn"], {})[count] = statement + stmt_offset = next( + i for i in stmt_nodes[path] if sources.is_inside_offset(offset, i) + ) + stmt_nodes[path].discard(stmt_offset) + statement_map[path].setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset pc_list[-1]["statement"] = count count += 1 except (KeyError, IndexError, StopIteration): @@ -757,16 +756,16 @@ def _check_left_operator(node: NodeBase, depth: int) -> bool: return op == "||" -def _convert_src(src): - src = [int(i) for i in src.split(":")[:2]] - return src[0], src[0] + src[1] +def _convert_src(src: str) -> Tuple[int, int]: + src_int = [int(i) for i in src.split(":")[:2]] + return src_int[0], src_int[0] + src_int[1] def _generate_vyper_coverage_data( source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: Dict -): +) -> Tuple: if not opcodes_str: - return {} + return {}, {}, {} source_map = deque(_expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) @@ -774,8 +773,17 @@ def _generate_vyper_coverage_data( (i["name"], _convert_src(i["src"])) for i in ast_json if i["ast_type"] == "FunctionDef" ) + stmt_nodes = set( + _convert_src(x["src"]) + for i in ast_json + if i["ast_type"] == "FunctionDef" + for x in i["body"] + ) + + statement_map: Dict = {} + pc_list: List = [] - pc = 0 + count, pc = 0, 0 while opcodes: @@ -798,10 +806,23 @@ def _generate_vyper_coverage_data( pc_list[-1]["path"] = source_str pc_list[-1]["offset"] = offset - fn = next((k for k, v in fn_offsets.items() if is_inside_offset(offset, v)), None) - if fn: + try: + if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: + pc_list[-1]["fn"] = pc_list[-2]["fn"] + continue + fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) + pc_list[-1]["fn"] = f"{contract_name}.{fn}" + stmt_offset = next(i for i in stmt_nodes if sources.is_inside_offset(offset, i)) + stmt_nodes.remove(stmt_offset) + statement_map.setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset + pc_list[-1]["statement"] = count + count += 1 + except (KeyError, IndexError, StopIteration): + pass + # TODO statement converage for public storage vars + # TODO branch coverage # TODO invalid reason # TODO nonpayable # TODO dev revert strings @@ -810,4 +831,4 @@ def _generate_vyper_coverage_data( pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] pc_map = dict((i.pop("pc"), i) for i in pc_list) - return pc_map + return pc_map, {source_str: statement_map}, {source_str: {}} From 9a28c39286a12eb7145a936a2185c81e9890fc51 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sat, 21 Dec 2019 21:39:04 +0400 Subject: [PATCH 08/39] vyper - better error string for payment to nonpayable function --- brownie/project/compiler.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index acd0578ec..bc23be873 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -809,6 +809,12 @@ def _generate_vyper_coverage_data( try: if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: pc_list[-1]["fn"] = pc_list[-2]["fn"] + if ( + pc_list[-1]["op"] == "REVERT" + and pc_list[-7]["op"] == "CALLVALUE" + and offset in fn_offsets.values() + ): + pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" continue fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) @@ -821,10 +827,8 @@ def _generate_vyper_coverage_data( except (KeyError, IndexError, StopIteration): pass - # TODO statement converage for public storage vars # TODO branch coverage # TODO invalid reason - # TODO nonpayable # TODO dev revert strings pc_list[0]["path"] = source_str From 024bda4ac56d441c4e0c88a3d73dbbe60be1b61a Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 22 Dec 2019 17:36:19 +0400 Subject: [PATCH 09/39] add dev revert strings for vyper contracts --- brownie/project/build.py | 10 ++++++---- brownie/project/compiler.py | 5 +---- docs/build-folder.rst | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/brownie/project/build.py b/brownie/project/build.py index 92a4fe93b..8d1995ee0 100644 --- a/brownie/project/build.py +++ b/brownie/project/build.py @@ -16,6 +16,7 @@ "deployedBytecode", "deployedSourceMap", "dependencies", + "language", "offset", "opcodes", "pcMap", @@ -44,14 +45,15 @@ def _add(self, build_json: Dict) -> None: if build_json["compiler"]["minify_source"]: build_json = self.expand_build_offsets(build_json) self._build[contract_name] = build_json - self._generate_revert_map(build_json["pcMap"]) + self._generate_revert_map(build_json["pcMap"], build_json["language"]) - def _generate_revert_map(self, pcMap: Dict) -> None: + def _generate_revert_map(self, pcMap: Dict, language: str) -> None: # Adds a contract's dev revert strings to the revert map and it's pcMap + marker = "//" if language == "Solidity" else "#" for pc, data in ( (k, v) for k, v in pcMap.items() - if v["op"] in {"REVERT", "INVALID"} or "jump_revert" in v + if v["op"] in ("REVERT", "INVALID") or "jump_revert" in v ): if "dev" not in data: if "fn" not in data or "first_revert" in data: @@ -60,7 +62,7 @@ def _generate_revert_map(self, pcMap: Dict) -> None: try: revert_str = self._sources.get(data["path"])[data["offset"][1] :] revert_str = revert_str[: revert_str.index("\n")] - revert_str = revert_str[revert_str.index("//") + 2 :].strip() + revert_str = revert_str[revert_str.index(marker) + len(marker) :].strip() if revert_str.startswith("dev:"): data["dev"] = revert_str except (KeyError, ValueError): diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index bc23be873..a822b8cbd 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -429,6 +429,7 @@ def generate_build_json( "coverageMap": {"statements": statement_map, "branches": branch_map}, "deployedBytecode": evm["deployedBytecode"]["object"], "deployedSourceMap": evm["deployedBytecode"]["sourceMap"], + "language": input_json["language"], "opcodes": evm["deployedBytecode"]["opcodes"], "pcMap": pc_map, "sha1": hash_, @@ -827,10 +828,6 @@ def _generate_vyper_coverage_data( except (KeyError, IndexError, StopIteration): pass - # TODO branch coverage - # TODO invalid reason - # TODO dev revert strings - pc_list[0]["path"] = source_str pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] pc_map = dict((i.pop("pc"), i) for i in pc_list) diff --git a/docs/build-folder.rst b/docs/build-folder.rst index 60e58ac0a..47ff80f11 100644 --- a/docs/build-folder.rst +++ b/docs/build-folder.rst @@ -27,6 +27,7 @@ Brownie generates compiler artifacts for each contract within a project, which a 'deployedBytecode': "0x00", // bytecode as hex string after deployment 'deployedSourceMap': "", // source mapping of the deployed bytecode 'dependencies': [], // contracts and libraries that this contract inherits from or is linked to + 'language': "", // source code language (Solidity or Vyper) 'offset': [], // source code offsets for this contract 'opcodes': "", // deployed contract opcodes list 'pcMap': [], // program counter map From 4a3c6d9c62969298befaa66acdaed60e090b07b3 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Sun, 22 Dec 2019 19:22:35 +0400 Subject: [PATCH 10/39] vyper - custom error strings for overflows and array index oob --- brownie/project/compiler.py | 40 +++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index a822b8cbd..00b9e7212 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -757,13 +757,8 @@ def _check_left_operator(node: NodeBase, depth: int) -> bool: return op == "||" -def _convert_src(src: str) -> Tuple[int, int]: - src_int = [int(i) for i in src.split(":")[:2]] - return src_int[0], src_int[0] + src_int[1] - - def _generate_vyper_coverage_data( - source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: Dict + source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: List ) -> Tuple: if not opcodes_str: return {}, {}, {} @@ -810,15 +805,20 @@ def _generate_vyper_coverage_data( try: if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: pc_list[-1]["fn"] = pc_list[-2]["fn"] - if ( - pc_list[-1]["op"] == "REVERT" - and pc_list[-7]["op"] == "CALLVALUE" - and offset in fn_offsets.values() - ): - pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" + if pc_list[-1]["op"] == "REVERT": + node = _find_node_by_offset(ast_json, offset) + if node["ast_type"] == "FunctionDef" and pc_list[-7]["op"] == "CALLVALUE": + pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" + elif node["ast_type"] == "Subscript": + pc_list[-1]["dev"] = "Index out of range" + elif node["ast_type"] in ("AugAssign", "BinOp"): + if node["op"]["ast_type"] == "Sub": + pc_list[-1]["dev"] = "Integer underflow" + else: + pc_list[-1]["dev"] = "Integer overflow" continue - fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) + fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) pc_list[-1]["fn"] = f"{contract_name}.{fn}" stmt_offset = next(i for i in stmt_nodes if sources.is_inside_offset(offset, i)) stmt_nodes.remove(stmt_offset) @@ -833,3 +833,17 @@ def _generate_vyper_coverage_data( pc_map = dict((i.pop("pc"), i) for i in pc_list) return pc_map, {source_str: statement_map}, {source_str: {}} + + +def _convert_src(src: str) -> Tuple[int, int]: + src_int = [int(i) for i in src.split(":")[:2]] + return src_int[0], src_int[0] + src_int[1] + + +def _find_node_by_offset(ast_json: List, offset: Tuple) -> Dict: + node = next(i for i in ast_json if sources.is_inside_offset(offset, _convert_src(i["src"]))) + if _convert_src(node["src"]) == offset: + return node + node_list = [i for i in node.values() if isinstance(i, dict) and "ast_type" in i] + node_list.extend([x for i in node.values() if isinstance(i, list) for x in i]) + return _find_node_by_offset(node_list, offset) From 0eef3288c65a8a0de529e237495c2cf5ad5e4031 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 00:09:11 +0400 Subject: [PATCH 11/39] vyper branch coverage --- brownie/project/compiler.py | 64 +++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/brownie/project/compiler.py b/brownie/project/compiler.py index 00b9e7212..5152b06eb 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler.py @@ -777,6 +777,7 @@ def _generate_vyper_coverage_data( ) statement_map: Dict = {} + branch_map: Dict = {} pc_list: List = [] count, pc = 0, 0 @@ -805,37 +806,58 @@ def _generate_vyper_coverage_data( try: if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: pc_list[-1]["fn"] = pc_list[-2]["fn"] - if pc_list[-1]["op"] == "REVERT": - node = _find_node_by_offset(ast_json, offset) - if node["ast_type"] == "FunctionDef" and pc_list[-7]["op"] == "CALLVALUE": - pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" - elif node["ast_type"] == "Subscript": - pc_list[-1]["dev"] = "Index out of range" - elif node["ast_type"] in ("AugAssign", "BinOp"): - if node["op"]["ast_type"] == "Sub": - pc_list[-1]["dev"] = "Integer underflow" - else: - pc_list[-1]["dev"] = "Integer overflow" - continue - - fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) - pc_list[-1]["fn"] = f"{contract_name}.{fn}" - stmt_offset = next(i for i in stmt_nodes if sources.is_inside_offset(offset, i)) - stmt_nodes.remove(stmt_offset) - statement_map.setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset - pc_list[-1]["statement"] = count - count += 1 + else: + # statement coverage + fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) + pc_list[-1]["fn"] = f"{contract_name}.{fn}" + stmt_offset = next(i for i in stmt_nodes if sources.is_inside_offset(offset, i)) + stmt_nodes.remove(stmt_offset) + statement_map.setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset + pc_list[-1]["statement"] = count + count += 1 except (KeyError, IndexError, StopIteration): pass + if pc_list[-1]["op"] not in ("JUMPI", "REVERT"): + continue + + node = _find_node_by_offset(ast_json, offset) + if pc_list[-1]["op"] == "REVERT": + # custom revert error strings + if node["ast_type"] == "FunctionDef" and pc_list[-7]["op"] == "CALLVALUE": + pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" + elif node["ast_type"] == "Subscript": + pc_list[-1]["dev"] = "Index out of range" + elif node["ast_type"] in ("AugAssign", "BinOp"): + if node["op"]["ast_type"] == "Sub": + pc_list[-1]["dev"] = "Integer underflow" + else: + pc_list[-1]["dev"] = "Integer overflow" + continue + + if node["ast_type"] in ("Assert", "If") or ( + node["ast_type"] == "Expr" and node["value"]["func"]["id"] == "assert_modifiable" + ): + # branch coverage + pc_list[-1]["branch"] = count + pc_list[-2]["branch"] = count + branch_map.setdefault(pc_list[-1]["fn"], {}) + if node["ast_type"] == "If": + branch_map[pc_list[-1]["fn"]][count] = _convert_src(node["test"]["src"]) + (False,) + else: + branch_map[pc_list[-1]["fn"]][count] = offset + (True,) + count += 1 + pc_list[0]["path"] = source_str pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] pc_map = dict((i.pop("pc"), i) for i in pc_list) - return pc_map, {source_str: statement_map}, {source_str: {}} + return pc_map, {source_str: statement_map}, {source_str: branch_map} def _convert_src(src: str) -> Tuple[int, int]: + if src is None: + return -1, -1 src_int = [int(i) for i in src.split(":")[:2]] return src_int[0], src_int[0] + src_int[1] From 28342bd654a383c0776a377a8ef9cb1dd4f569a4 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 03:06:47 +0400 Subject: [PATCH 12/39] split project.compiler into multiple files --- brownie/project/compiler/__init__.py | 231 +++++++++ .../{compiler.py => compiler/solidity.py} | 483 +++--------------- brownie/project/compiler/utils.py | 25 + brownie/project/compiler/vyper.py | 172 +++++++ tests/project/test_compiler.py | 4 +- 5 files changed, 509 insertions(+), 406 deletions(-) create mode 100644 brownie/project/compiler/__init__.py rename brownie/project/{compiler.py => compiler/solidity.py} (57%) create mode 100644 brownie/project/compiler/utils.py create mode 100644 brownie/project/compiler/vyper.py diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py new file mode 100644 index 000000000..52f982aa0 --- /dev/null +++ b/brownie/project/compiler/__init__.py @@ -0,0 +1,231 @@ +#!/usr/bin/python3 + +from copy import deepcopy +from typing import Dict, Optional, Union + +from semantic_version import Version + +from brownie.project import sources +from brownie.project.compiler.solidity import ( # NOQA: F401 + find_best_solc_version, + find_solc_versions, + install_solc, + set_solc_version, +) + +from . import solidity, vyper + +STANDARD_JSON: Dict = { + "language": None, + "sources": {}, + "settings": { + "outputSelection": { + "*": {"*": ["abi", "evm.bytecode", "evm.deployedBytecode"], "": ["ast"]} + }, + "optimizer": {"enabled": True, "runs": 200}, + "evmVersion": None, + "remappings": [], + }, +} +EVM_SOLC_VERSIONS = [ + # ("istanbul", Version("0.5.13")), # TODO enable when ganache istanbul support is out of beta + ("petersburg", Version("0.5.5")), + ("byzantium", Version("0.4.0")), +] + + +def compile_and_format( + contract_sources: Dict[str, str], + solc_version: Optional[str] = None, + optimize: bool = True, + runs: int = 200, + evm_version: int = None, + minify: bool = False, + silent: bool = True, + allow_paths: Optional[str] = None, +) -> Dict: + """Compiles contracts and returns build data. + + Args: + contracts: a dictionary in the form of {'path': "source code"} + solc_version: solc version to compile with (use None to set via pragmas) + optimize: enable solc optimizer + runs: optimizer runs + evm_version: evm version to compile for + minify: minify source files + silent: verbose reporting + allow_paths: compiler allowed filesystem import path + + Returns: + build data dict + """ + if not contract_sources: + return {} + + if list(contract_sources)[0].endswith(".vy"): + path_versions = {"vyper": list(contract_sources)} + elif solc_version is not None: + path_versions = {solc_version: list(contract_sources)} + else: + path_versions = find_solc_versions(contract_sources, install_needed=True, silent=silent) + + build_json: Dict = {} + for version, path_list in path_versions.items(): + if version == "vyper": + language = "Vyper" + compiler_data = {"minify_source": False, "version": str(vyper.get_version())} + else: + set_solc_version(version) + language = "Solidity" + compiler_data = {"minify_source": minify, "version": str(solidity.get_version())} + + to_compile = dict((k, v) for k, v in contract_sources.items() if k in path_list) + + input_json = generate_input_json(to_compile, optimize, runs, evm_version, minify, language) + output_json = compile_from_input_json(input_json, silent, allow_paths) + build_json.update(generate_build_json(input_json, output_json, compiler_data, silent)) + return build_json + + +def generate_input_json( + contract_sources: Dict[str, str], + optimize: bool = True, + runs: int = 200, + evm_version: Union[int, str, None] = None, + minify: bool = False, + language: str = "Solidity", +) -> Dict: + + """Formats contracts to the standard solc input json. + + Args: + contract_sources: a dictionary in the form of {path: 'source code'} + optimize: enable solc optimizer + runs: optimizer runs + evm_version: evm version to compile for + minify: should source code be minified? + language: source language (Solidity or Vyper) + + Returns: dict + """ + + if language not in ("Solidity", "Vyper"): + raise ValueError(f"Unsupported language: {language}") + + if evm_version is None: + if language == "Solidity": + evm_version = next(i[0] for i in EVM_SOLC_VERSIONS if solidity.get_version() >= i[1]) + else: + evm_version = "byzantium" + + input_json: Dict = deepcopy(STANDARD_JSON) + input_json["language"] = language + input_json["settings"]["optimizer"]["enabled"] = optimize + input_json["settings"]["optimizer"]["runs"] = runs if optimize else 0 + input_json["settings"]["evmVersion"] = evm_version + if language == "Vyper": + input_json["outputSelection"] = input_json["settings"].pop("outputSelection") + input_json["sources"] = dict( + (k, {"content": sources.minify(v)[0] if minify else v}) for k, v in contract_sources.items() + ) + return input_json + + +def compile_from_input_json( + input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None +) -> Dict: + + """ + Compiles contracts from a standard input json. + + Args: + input_json: solc input json + silent: verbose reporting + allow_paths: compiler allowed filesystem import path + + Returns: standard compiler output json + """ + + if input_json["language"] == "Vyper": + return vyper.compile_from_input_json(input_json, silent, allow_paths) + if input_json["language"] == "Solidity": + return solidity.compile_from_input_json(input_json, silent, allow_paths) + raise ValueError(f"Unsupported language: {input_json['language']}") + + +def generate_build_json( + input_json: Dict, output_json: Dict, compiler_data: Optional[Dict] = None, silent: bool = True +) -> Dict: + """Formats standard compiler output to the brownie build json. + + Args: + input_json: solc input json used to compile + output_json: output json returned by compiler + compiler_data: additonal data to include under 'compiler' in build json + silent: verbose reporting + + Returns: build json dict""" + if not silent: + print("Generating build data...") + + if compiler_data is None: + compiler_data = {} + compiler_data.update( + { + "optimize": input_json["settings"]["optimizer"]["enabled"], + "runs": input_json["settings"]["optimizer"]["runs"], + "evm_version": input_json["settings"]["evmVersion"], + } + ) + minified = "minify_source" in compiler_data and compiler_data["minify_source"] + build_json = {} + path_list = list(input_json["sources"]) + + if input_json["language"] == "Solidity": + source_nodes, statement_nodes, branch_nodes = solidity._get_nodes(output_json) + + for path, contract_name in [(k, v) for k in path_list for v in output_json["contracts"][k]]: + + if not silent: + print(f" - {contract_name}...") + + abi = output_json["contracts"][path][contract_name]["abi"] + output_evm = output_json["contracts"][path][contract_name]["evm"] + hash_ = sources.get_hash(input_json["sources"][path]["content"], contract_name, minified) + + if input_json["language"] == "Solidity": + contract_node = next(i[contract_name] for i in source_nodes if i.absolutePath == path) + build_json[contract_name] = solidity._get_unique_build_json( + output_evm, + contract_node, + statement_nodes, + branch_nodes, + next((True for i in abi if i["type"] == "fallback"), False), + ) + + else: + build_json[contract_name] = vyper._get_unique_build_json( + output_evm, path, contract_name, output_json["sources"][path]["ast"] + ) + + build_json[contract_name].update( + { + "abi": abi, + "ast": output_json["sources"][path]["ast"], + "compiler": compiler_data, + "contractName": contract_name, + "deployedBytecode": output_evm["deployedBytecode"]["object"], + "deployedSourceMap": output_evm["deployedBytecode"]["sourceMap"], + "language": input_json["language"], + "opcodes": output_evm["deployedBytecode"]["opcodes"], + "sha1": hash_, + "source": input_json["sources"][path]["content"], + "sourceMap": output_evm["bytecode"].get("sourceMap", ""), + "sourcePath": path, + } + ) + + if not silent: + print("") + + return build_json diff --git a/brownie/project/compiler.py b/brownie/project/compiler/solidity.py similarity index 57% rename from brownie/project/compiler.py rename to brownie/project/compiler/solidity.py index 5152b06eb..a17dc5ef3 100644 --- a/brownie/project/compiler.py +++ b/brownie/project/compiler/solidity.py @@ -3,19 +3,17 @@ import logging import re from collections import deque -from copy import deepcopy from hashlib import sha1 -from typing import Any, Dict, List, Optional, Set, Tuple, Union +from typing import Any, Dict, List, Optional, Set, Tuple import solcast import solcx -import vyper from requests.exceptions import ConnectionError from semantic_version import NpmSpec, Version from solcast.nodes import NodeBase -from vyper.cli import vyper_json from brownie.exceptions import CompilerError, IncompatibleSolcVersion, PragmaError +from brownie.project.compiler.utils import expand_source_map from . import sources @@ -26,25 +24,53 @@ sh.setFormatter(logging.Formatter("%(message)s")) solcx_logger.addHandler(sh) -STANDARD_JSON: Dict = { - "language": None, - "sources": {}, - "settings": { - "outputSelection": { - "*": {"*": ["abi", "evm.bytecode", "evm.deployedBytecode"], "": ["ast"]} - }, - "optimizer": {"enabled": True, "runs": 200}, - "evmVersion": None, - "remappings": [], - }, -} PRAGMA_REGEX = re.compile(r"pragma +solidity([^;]*);") AVAILABLE_SOLC_VERSIONS = None -EVM_SOLC_VERSIONS = [ - # ("istanbul", Version("0.5.13")), # TODO enable when ganache istanbul support is out of beta - ("petersburg", Version("0.5.5")), - ("byzantium", Version("0.4.0")), -] + + +def get_version() -> Version: + return solcx.get_solc_version() + + +def compile_from_input_json( + input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None +) -> Dict: + + """ + Compiles contracts from a standard input json. + + Args: + input_json: solc input json + silent: verbose reporting + allow_paths: compiler allowed filesystem import path + + Returns: standard compiler output json + """ + + optimizer = input_json["settings"]["optimizer"] + input_json["settings"].setdefault("evmVersion", None) + + if not silent: + print("Compiling contracts...") + print(f" Solc version: {str(solcx.get_solc_version())}") + + print( + " Optimizer: " + + (f"Enabled Runs: {optimizer['runs']}" if optimizer["enabled"] else "Disabled") + ) + if input_json["settings"]["evmVersion"]: + print(f" EVM Version: {input_json['settings']['evmVersion'].capitalize()}") + + try: + return solcx.compile_standard( + input_json, + optimize=optimizer["enabled"], + optimize_runs=optimizer["runs"], + evm_version=input_json["settings"]["evmVersion"], + allow_paths=allow_paths, + ) + except solcx.exceptions.SolcError as e: + raise CompilerError(e) def set_solc_version(version: str) -> str: @@ -65,59 +91,6 @@ def install_solc(*versions: str) -> None: solcx.install_solc(str(version)) -def compile_and_format( - contract_sources: Dict[str, str], - solc_version: Optional[str] = None, - optimize: bool = True, - runs: int = 200, - evm_version: int = None, - minify: bool = False, - silent: bool = True, - allow_paths: Optional[str] = None, -) -> Dict: - """Compiles contracts and returns build data. - - Args: - contracts: a dictionary in the form of {'path': "source code"} - solc_version: solc version to compile with (use None to set via pragmas) - optimize: enable solc optimizer - runs: optimizer runs - evm_version: evm version to compile for - minify: minify source files - silent: verbose reporting - allow_paths: compiler allowed filesystem import path - - Returns: - build data dict - """ - if not contract_sources: - return {} - - if list(contract_sources)[0].endswith(".vy"): - path_versions = {"vyper": list(contract_sources)} - elif solc_version is not None: - path_versions = {solc_version: list(contract_sources)} - else: - path_versions = find_solc_versions(contract_sources, install_needed=True, silent=silent) - - build_json: Dict = {} - for version, path_list in path_versions.items(): - if version == "vyper": - language = "Vyper" - compiler_data = {"minify_source": False, "version": vyper.__version__} - else: - set_solc_version(version) - language = "Solidity" - compiler_data = {"minify_source": minify, "version": str(solcx.get_solc_version())} - - to_compile = dict((k, v) for k, v in contract_sources.items() if k in path_list) - - input_json = generate_input_json(to_compile, optimize, runs, evm_version, minify, language) - output_json = compile_from_input_json(input_json, silent, allow_paths) - build_json.update(generate_build_json(input_json, output_json, compiler_data, silent)) - return build_json - - def find_solc_versions( contract_sources: Dict[str, str], install_needed: bool = False, @@ -251,198 +224,35 @@ def _get_solc_version_list() -> Tuple[List, List]: return AVAILABLE_SOLC_VERSIONS, installed_versions -def generate_input_json( - contract_sources: Dict[str, str], - optimize: bool = True, - runs: int = 200, - evm_version: Union[int, str, None] = None, - minify: bool = False, - language: str = "Solidity", +def _get_unique_build_json( + output_evm: Dict, contract_node: Any, stmt_nodes: Dict, branch_nodes: Dict, has_fallback: bool ) -> Dict: - """Formats contracts to the standard solc input json. - - Args: - contract_sources: a dictionary in the form of {path: 'source code'} - optimize: enable solc optimizer - runs: optimizer runs - evm_version: evm version to compile for - minify: should source code be minified? - - Returns: dict - """ - - if evm_version is None: - if language == "Solidity": - evm_version = next(i[0] for i in EVM_SOLC_VERSIONS if solcx.get_solc_version() >= i[1]) - else: - evm_version = "byzantium" - - input_json: Dict = deepcopy(STANDARD_JSON) - input_json["language"] = language - input_json["settings"]["optimizer"]["enabled"] = optimize - input_json["settings"]["optimizer"]["runs"] = runs if optimize else 0 - input_json["settings"]["evmVersion"] = evm_version - if language == "Vyper": - input_json["outputSelection"] = input_json["settings"].pop("outputSelection") - input_json["sources"] = dict( - (k, {"content": sources.minify(v)[0] if minify else v}) for k, v in contract_sources.items() - ) - return input_json - - -def compile_from_input_json( - input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None -) -> Dict: - - """ - Compiles contracts from a standard input json. - - Args: - input_json: solc input json - silent: verbose reporting - allow_paths: compiler allowed filesystem import path - - Returns: standard compiler output json - """ - - optimizer = input_json["settings"]["optimizer"] - input_json["settings"].setdefault("evmVersion", None) - - if not silent: - print("Compiling contracts...") - if input_json["language"] == "Vyper": - print(f" Vyper version: {vyper.__version__}") - else: - print(f" Solc version: {str(solcx.get_solc_version())}") - - print( - " Optimizer: " - + (f"Enabled Runs: {optimizer['runs']}" if optimizer["enabled"] else "Disabled") - ) - if input_json["settings"]["evmVersion"]: - print(f" EVM Version: {input_json['settings']['evmVersion'].capitalize()}") - - if input_json["language"] == "Vyper": - return vyper_json.compile_json(input_json, root_path=allow_paths) - - try: - return solcx.compile_standard( - input_json, - optimize=optimizer["enabled"], - optimize_runs=optimizer["runs"], - evm_version=input_json["settings"]["evmVersion"], - allow_paths=allow_paths, + paths = sorted( + set( + [contract_node.parent().absolutePath] + + [i.parent().absolutePath for i in contract_node.dependencies] ) - except solcx.exceptions.SolcError as e: - raise CompilerError(e) - - -def generate_build_json( - input_json: Dict, output_json: Dict, compiler_data: Optional[Dict] = None, silent: bool = True -) -> Dict: - """Formats standard compiler output to the brownie build json. - - Args: - input_json: solc input json used to compile - output_json: output json returned by compiler - compiler_data: additonal data to include under 'compiler' in build json - silent: verbose reporting - - Returns: build json dict""" - if not silent: - print("Generating build data...") - - if compiler_data is None: - compiler_data = {} - compiler_data.update( - { - "optimize": input_json["settings"]["optimizer"]["enabled"], - "runs": input_json["settings"]["optimizer"]["runs"], - "evm_version": input_json["settings"]["evmVersion"], - } ) - minified = "minify_source" in compiler_data and compiler_data["minify_source"] - build_json = {} - path_list = list(input_json["sources"]) - - if input_json["language"] == "Solidity": - source_nodes = solcast.from_standard_output(output_json) - statement_nodes = _get_statement_nodes(source_nodes) - branch_nodes = _get_branch_nodes(source_nodes) - - for path, contract_name in [(k, v) for k in path_list for v in output_json["contracts"][k]]: - - if not silent: - print(f" - {contract_name}...") - - abi = output_json["contracts"][path][contract_name]["abi"] - evm = output_json["contracts"][path][contract_name]["evm"] - bytecode = _format_link_references(evm) - hash_ = sources.get_hash(input_json["sources"][path]["content"], contract_name, minified) - - if input_json["language"] == "Solidity": - node = next(i[contract_name] for i in source_nodes if i.absolutePath == path) - paths = sorted( - set( - [node.parent().absolutePath] - + [i.parent().absolutePath for i in node.dependencies] - ) - ) - pc_map, statement_map, branch_map = _generate_solc_coverage_data( - evm["deployedBytecode"]["sourceMap"], - evm["deployedBytecode"]["opcodes"], - node, - statement_nodes, - branch_nodes, - next((True for i in abi if i["type"] == "fallback"), False), - ) - build_json[contract_name] = { - "allSourcePaths": paths, - "dependencies": [i.name for i in node.dependencies], - "offset": node.offset, - "type": node.contractKind, - } - else: - pc_map, statement_map, branch_map = _generate_vyper_coverage_data( - evm["deployedBytecode"]["sourceMap"], - evm["deployedBytecode"]["opcodes"], - path, - contract_name, - output_json["sources"][path]["ast"], - ) - build_json[contract_name] = { - "allSourcePaths": [path], - "dependencies": [], - "offset": [0, len(input_json["sources"][path]["content"])], - "type": "contract", - } - - build_json[contract_name].update( - { - "abi": abi, - "ast": output_json["sources"][path]["ast"], - "bytecode": bytecode, - "bytecodeSha1": _get_bytecode_hash(bytecode), - "compiler": compiler_data, - "contractName": contract_name, - "coverageMap": {"statements": statement_map, "branches": branch_map}, - "deployedBytecode": evm["deployedBytecode"]["object"], - "deployedSourceMap": evm["deployedBytecode"]["sourceMap"], - "language": input_json["language"], - "opcodes": evm["deployedBytecode"]["opcodes"], - "pcMap": pc_map, - "sha1": hash_, - "source": input_json["sources"][path]["content"], - "sourceMap": evm["bytecode"].get("sourceMap", ""), - "sourcePath": path, - } - ) - - if not silent: - print("") - - return build_json + bytecode = _format_link_references(output_evm) + pc_map, statement_map, branch_map = _generate_coverage_data( + output_evm["deployedBytecode"]["sourceMap"], + output_evm["deployedBytecode"]["opcodes"], + contract_node, + stmt_nodes, + branch_nodes, + has_fallback, + ) + return { + "allSourcePaths": paths, + "bytecode": bytecode, + "bytecodeSha1": sha1(bytecode[:-68].encode()).hexdigest(), + "coverageMap": {"statements": statement_map, "branches": branch_map}, + "dependencies": [i.name for i in contract_node.dependencies], + "offset": contract_node.offset, + "pcMap": pc_map, + "type": contract_node.contractKind, + } def _format_link_references(evm: Dict) -> Dict: @@ -456,12 +266,7 @@ def _format_link_references(evm: Dict) -> Dict: return bytecode -def _get_bytecode_hash(bytecode: Dict) -> str: - # Returns a sha1 hash of the given bytecode without metadata - return sha1(bytecode[:-68].encode()).hexdigest() - - -def _generate_solc_coverage_data( +def _generate_coverage_data( source_map_str: str, opcodes_str: str, contract_node: Any, @@ -473,7 +278,7 @@ def _generate_solc_coverage_data( if not opcodes_str: return {}, {}, {} - source_map = deque(_expand_source_map(source_map_str)) + source_map = deque(expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) contract_nodes = [contract_node] + contract_node.dependencies @@ -497,7 +302,6 @@ def _generate_solc_coverage_data( fallback_hexstr: str = "unassigned" while source_map: - # format of source is [start, stop, contract_id, jump code] source = source_map.popleft() pc_list.append({"op": opcodes.popleft(), "pc": pc}) @@ -636,26 +440,11 @@ def _get_fn_full_name(source_node: NodeBase, offset: Tuple[int, int]) -> str: return f"{node.parent().name}.{name}" -def _expand_source_map(source_map_str: str) -> List: - # Expands the compressed sourceMap supplied by solc into a list of lists - source_map: List = [_expand_row(i) if i else None for i in source_map_str.split(";")] - for i, value in enumerate(source_map[1:], 1): - if value is None: - source_map[i] = source_map[i - 1] - continue - for x in range(4): - if source_map[i][x] is None: - source_map[i][x] = source_map[i - 1][x] - return source_map - - -def _expand_row(row: str) -> List: - result: List = [None] * 4 - # ignore the new "modifier depth" value in solidity 0.6.0 - for i, value in enumerate(row.split(":")[:4]): - if value: - result[i] = value if i == 3 else int(value) - return result +def _get_nodes(output_json: Dict) -> Tuple[Dict, Dict, Dict]: + source_nodes = solcast.from_standard_output(output_json) + stmt_nodes = _get_statement_nodes(source_nodes) + branch_nodes = _get_branch_nodes(source_nodes) + return source_nodes, stmt_nodes, branch_nodes def _get_statement_nodes(source_nodes: Dict) -> Dict: @@ -755,117 +544,3 @@ def _check_left_operator(node: NodeBase, depth: int) -> bool: i for i in parents if i.leftExpression == node or node.is_child_of(i.leftExpression) ).operator return op == "||" - - -def _generate_vyper_coverage_data( - source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: List -) -> Tuple: - if not opcodes_str: - return {}, {}, {} - - source_map = deque(_expand_source_map(source_map_str)) - opcodes = deque(opcodes_str.split(" ")) - fn_offsets = dict( - (i["name"], _convert_src(i["src"])) for i in ast_json if i["ast_type"] == "FunctionDef" - ) - - stmt_nodes = set( - _convert_src(x["src"]) - for i in ast_json - if i["ast_type"] == "FunctionDef" - for x in i["body"] - ) - - statement_map: Dict = {} - branch_map: Dict = {} - - pc_list: List = [] - count, pc = 0, 0 - - while opcodes: - - # format of source is [start, stop, contract_id, jump code] - source = source_map.popleft() - pc_list.append({"op": opcodes.popleft(), "pc": pc}) - - if source[3] != "-": - pc_list[-1]["jump"] = source[3] - - pc += 1 - if opcodes and opcodes[0][:2] == "0x": - pc_list[-1]["value"] = opcodes.popleft() - pc += int(pc_list[-1]["op"][4:]) - - # set source offset (-1 means none) - if source[0] == -1: - continue - offset = (source[0], source[0] + source[1]) - pc_list[-1]["path"] = source_str - pc_list[-1]["offset"] = offset - - try: - if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: - pc_list[-1]["fn"] = pc_list[-2]["fn"] - else: - # statement coverage - fn = next(k for k, v in fn_offsets.items() if sources.is_inside_offset(offset, v)) - pc_list[-1]["fn"] = f"{contract_name}.{fn}" - stmt_offset = next(i for i in stmt_nodes if sources.is_inside_offset(offset, i)) - stmt_nodes.remove(stmt_offset) - statement_map.setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset - pc_list[-1]["statement"] = count - count += 1 - except (KeyError, IndexError, StopIteration): - pass - - if pc_list[-1]["op"] not in ("JUMPI", "REVERT"): - continue - - node = _find_node_by_offset(ast_json, offset) - if pc_list[-1]["op"] == "REVERT": - # custom revert error strings - if node["ast_type"] == "FunctionDef" and pc_list[-7]["op"] == "CALLVALUE": - pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" - elif node["ast_type"] == "Subscript": - pc_list[-1]["dev"] = "Index out of range" - elif node["ast_type"] in ("AugAssign", "BinOp"): - if node["op"]["ast_type"] == "Sub": - pc_list[-1]["dev"] = "Integer underflow" - else: - pc_list[-1]["dev"] = "Integer overflow" - continue - - if node["ast_type"] in ("Assert", "If") or ( - node["ast_type"] == "Expr" and node["value"]["func"]["id"] == "assert_modifiable" - ): - # branch coverage - pc_list[-1]["branch"] = count - pc_list[-2]["branch"] = count - branch_map.setdefault(pc_list[-1]["fn"], {}) - if node["ast_type"] == "If": - branch_map[pc_list[-1]["fn"]][count] = _convert_src(node["test"]["src"]) + (False,) - else: - branch_map[pc_list[-1]["fn"]][count] = offset + (True,) - count += 1 - - pc_list[0]["path"] = source_str - pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] - pc_map = dict((i.pop("pc"), i) for i in pc_list) - - return pc_map, {source_str: statement_map}, {source_str: branch_map} - - -def _convert_src(src: str) -> Tuple[int, int]: - if src is None: - return -1, -1 - src_int = [int(i) for i in src.split(":")[:2]] - return src_int[0], src_int[0] + src_int[1] - - -def _find_node_by_offset(ast_json: List, offset: Tuple) -> Dict: - node = next(i for i in ast_json if sources.is_inside_offset(offset, _convert_src(i["src"]))) - if _convert_src(node["src"]) == offset: - return node - node_list = [i for i in node.values() if isinstance(i, dict) and "ast_type" in i] - node_list.extend([x for i in node.values() if isinstance(i, list) for x in i]) - return _find_node_by_offset(node_list, offset) diff --git a/brownie/project/compiler/utils.py b/brownie/project/compiler/utils.py new file mode 100644 index 000000000..95e3bf844 --- /dev/null +++ b/brownie/project/compiler/utils.py @@ -0,0 +1,25 @@ +#!/usr/bin/python3 + +from typing import List + + +def expand_source_map(source_map_str: str) -> List: + # Expands the compressed sourceMap supplied by solc into a list of lists + source_map: List = [_expand_row(i) if i else None for i in source_map_str.split(";")] + for i, value in enumerate(source_map[1:], 1): + if value is None: + source_map[i] = source_map[i - 1] + continue + for x in range(4): + if source_map[i][x] is None: + source_map[i][x] = source_map[i - 1][x] + return source_map + + +def _expand_row(row: str) -> List: + result: List = [None] * 4 + # ignore the new "modifier depth" value in solidity 0.6.0 + for i, value in enumerate(row.split(":")[:4]): + if value: + result[i] = value if i == 3 else int(value) + return result diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py new file mode 100644 index 000000000..ec9f6904e --- /dev/null +++ b/brownie/project/compiler/vyper.py @@ -0,0 +1,172 @@ +#!/usr/bin/python3 + +from collections import deque +from hashlib import sha1 +from typing import Dict, List, Optional, Tuple + +import vyper +from semantic_version import Version +from vyper.cli import vyper_json + +from brownie.project.compiler.utils import expand_source_map +from brownie.project.sources import is_inside_offset + + +def get_version() -> Version: + return Version.coerce(vyper.__version__) + + +def compile_from_input_json( + input_json: Dict, silent: bool = True, allow_paths: Optional[str] = None +) -> Dict: + + """ + Compiles contracts from a standard input json. + + Args: + input_json: solc input json + silent: verbose reporting + allow_paths: compiler allowed filesystem import path + + Returns: standard compiler output json + """ + + if not silent: + print("Compiling contracts...") + print(f" Vyper version: {get_version()}") + return vyper_json.compile_json(input_json, allow_paths) + + +def _get_unique_build_json( + output_evm: Dict, source_str: str, contract_name: str, ast_json: List +) -> Dict: + pc_map, statement_map, branch_map = _generate_coverage_data( + output_evm["deployedBytecode"]["sourceMap"], + output_evm["deployedBytecode"]["opcodes"], + source_str, + contract_name, + ast_json, + ) + return { + "allSourcePaths": [source_str], + "bytecode": output_evm["bytecode"]["object"], + "bytecodeSha1": sha1(output_evm["bytecode"]["object"].encode()).hexdigest(), + "coverageMap": {"statements": statement_map, "branches": branch_map}, + "dependencies": [], + "offset": pc_map[0]["offset"], + "pcMap": pc_map, + "type": "contract", + } + + +def _generate_coverage_data( + source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: List +) -> Tuple: + if not opcodes_str: + return {}, {}, {} + + source_map = deque(expand_source_map(source_map_str)) + opcodes = deque(opcodes_str.split(" ")) + fn_offsets = dict( + (i["name"], _convert_src(i["src"])) for i in ast_json if i["ast_type"] == "FunctionDef" + ) + + stmt_nodes = set( + _convert_src(x["src"]) + for i in ast_json + if i["ast_type"] == "FunctionDef" + for x in i["body"] + ) + + statement_map: Dict = {} + branch_map: Dict = {} + + pc_list: List = [] + count, pc = 0, 0 + + while opcodes: + # format of source is [start, stop, contract_id, jump code] + source = source_map.popleft() + pc_list.append({"op": opcodes.popleft(), "pc": pc}) + + if source[3] != "-": + pc_list[-1]["jump"] = source[3] + + pc += 1 + if opcodes and opcodes[0][:2] == "0x": + pc_list[-1]["value"] = opcodes.popleft() + pc += int(pc_list[-1]["op"][4:]) + + # set source offset (-1 means none) + if source[0] == -1: + continue + offset = (source[0], source[0] + source[1]) + pc_list[-1]["path"] = source_str + pc_list[-1]["offset"] = offset + + try: + if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: + pc_list[-1]["fn"] = pc_list[-2]["fn"] + else: + # statement coverage + fn = next(k for k, v in fn_offsets.items() if is_inside_offset(offset, v)) + pc_list[-1]["fn"] = f"{contract_name}.{fn}" + stmt_offset = next(i for i in stmt_nodes if is_inside_offset(offset, i)) + stmt_nodes.remove(stmt_offset) + statement_map.setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset + pc_list[-1]["statement"] = count + count += 1 + except (KeyError, IndexError, StopIteration): + pass + + if pc_list[-1]["op"] not in ("JUMPI", "REVERT"): + continue + + node = _find_node_by_offset(ast_json, offset) + if pc_list[-1]["op"] == "REVERT": + # custom revert error strings + if node["ast_type"] == "FunctionDef" and pc_list[-7]["op"] == "CALLVALUE": + pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" + elif node["ast_type"] == "Subscript": + pc_list[-1]["dev"] = "Index out of range" + elif node["ast_type"] in ("AugAssign", "BinOp"): + if node["op"]["ast_type"] == "Sub": + pc_list[-1]["dev"] = "Integer underflow" + else: + pc_list[-1]["dev"] = "Integer overflow" + continue + + if node["ast_type"] in ("Assert", "If") or ( + node["ast_type"] == "Expr" and node["value"]["func"]["id"] == "assert_modifiable" + ): + # branch coverage + pc_list[-1]["branch"] = count + pc_list[-2]["branch"] = count + branch_map.setdefault(pc_list[-1]["fn"], {}) + if node["ast_type"] == "If": + branch_map[pc_list[-1]["fn"]][count] = _convert_src(node["test"]["src"]) + (False,) + else: + branch_map[pc_list[-1]["fn"]][count] = offset + (True,) + count += 1 + + pc_list[0]["path"] = source_str + pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] + pc_map = dict((i.pop("pc"), i) for i in pc_list) + + return pc_map, {source_str: statement_map}, {source_str: branch_map} + + +def _convert_src(src: str) -> Tuple[int, int]: + if src is None: + return -1, -1 + src_int = [int(i) for i in src.split(":")[:2]] + return src_int[0], src_int[0] + src_int[1] + + +def _find_node_by_offset(ast_json: List, offset: Tuple) -> Dict: + node = next(i for i in ast_json if is_inside_offset(offset, _convert_src(i["src"]))) + if _convert_src(node["src"]) == offset: + return node + node_list = [i for i in node.values() if isinstance(i, dict) and "ast_type" in i] + node_list.extend([x for i in node.values() if isinstance(i, list) for x in i]) + return _find_node_by_offset(node_list, offset) diff --git a/tests/project/test_compiler.py b/tests/project/test_compiler.py index b8600eaca..959857ce4 100644 --- a/tests/project/test_compiler.py +++ b/tests/project/test_compiler.py @@ -121,9 +121,9 @@ def test_build_json_unlinked_libraries(solc4source, solc5source): def test_format_link_references(solc4json, solc5json): evm = solc5json["contracts"]["path"]["Foo"]["evm"] - assert "__Bar__" in compiler._format_link_references(evm) + assert "__Bar__" in compiler.solidity._format_link_references(evm) evm = solc4json["contracts"]["path"]["Foo"]["evm"] - assert "__Bar__" in compiler._format_link_references(evm) + assert "__Bar__" in compiler.solidity._format_link_references(evm) def test_compiler_errors(solc4source, solc5source): From 51136c823d52e3dfbfca77607bd88670b53e7e17 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 03:29:13 +0400 Subject: [PATCH 13/39] handle solc and vyper in the same call to compile_and_format --- brownie/project/compiler/__init__.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index 52f982aa0..7cf73c894 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -62,15 +62,23 @@ def compile_and_format( if not contract_sources: return {} - if list(contract_sources)[0].endswith(".vy"): - path_versions = {"vyper": list(contract_sources)} - elif solc_version is not None: - path_versions = {solc_version: list(contract_sources)} - else: - path_versions = find_solc_versions(contract_sources, install_needed=True, silent=silent) - build_json: Dict = {} - for version, path_list in path_versions.items(): + compiler_targets = {} + + vyper_paths = [i for i in contract_sources if i.endswith(".vy")] + if vyper_paths: + compiler_targets["vyper"] = vyper_paths + + solc_sources = dict((k, v) for k, v in contract_sources.items() if k.endswith(".sol")) + if solc_sources: + if solc_version is None: + compiler_targets.update( + find_solc_versions(solc_sources, install_needed=True, silent=silent) + ) + else: + compiler_targets[solc_version] = list(solc_sources) + + for version, path_list in compiler_targets.items(): if version == "vyper": language = "Vyper" compiler_data = {"minify_source": False, "version": str(vyper.get_version())} From e20303f8c3dab0e4bc6859294e6b99d321be4772 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 12:17:11 +0400 Subject: [PATCH 14/39] fix failing tests, add explicit error for sources with wrong suffix --- brownie/project/compiler/__init__.py | 3 +++ brownie/project/main.py | 2 +- tests/project/test_compiler.py | 36 ++++++++++++++-------------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index 7cf73c894..e664be2c1 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -62,6 +62,9 @@ def compile_and_format( if not contract_sources: return {} + if [i for i in contract_sources if not i.endswith(".sol") and not i.endswith(".vy")]: + raise ValueError("Source filenames must end in .sol or .vy") + build_json: Dict = {} compiler_targets = {} diff --git a/brownie/project/main.py b/brownie/project/main.py index ba5183209..1ace1e685 100644 --- a/brownie/project/main.py +++ b/brownie/project/main.py @@ -398,7 +398,7 @@ def compile_source( "evm_version": evm_version, "minify_source": False, } - return TempProject("TempProject", {"": source}, compiler_config) + return TempProject("TempProject", {".sol": source}, compiler_config) def load(project_path: Union[Path, str, None] = None, name: Optional[str] = None) -> "Project": diff --git a/tests/project/test_compiler.py b/tests/project/test_compiler.py index 959857ce4..c095ba6ac 100644 --- a/tests/project/test_compiler.py +++ b/tests/project/test_compiler.py @@ -13,14 +13,14 @@ @pytest.fixture def solc4json(solc4source): compiler.set_solc_version("0.4.25") - input_json = compiler.generate_input_json({"path": solc4source}, True, 200) + input_json = compiler.generate_input_json({"path.sol": solc4source}, True, 200) yield compiler.compile_from_input_json(input_json) @pytest.fixture def solc5json(solc5source): compiler.set_solc_version("0.5.7") - input_json = compiler.generate_input_json({"path": solc5source}, True, 200) + input_json = compiler.generate_input_json({"path.sol": solc5source}, True, 200) yield compiler.compile_from_input_json(input_json) @@ -50,21 +50,21 @@ def test_set_solc_version(): def test_generate_input_json(solc5source): - input_json = compiler.generate_input_json({"path": solc5source}, True, 200) + input_json = compiler.generate_input_json({"path.sol": solc5source}, True, 200) assert input_json["settings"]["optimizer"]["enabled"] is True assert input_json["settings"]["optimizer"]["runs"] == 200 - assert input_json["sources"]["path"]["content"] == solc5source + assert input_json["sources"]["path.sol"]["content"] == solc5source input_json = compiler.generate_input_json( - {"path": solc5source}, optimize=False, runs=0, minify=True + {"path.sol": solc5source}, optimize=False, runs=0, minify=True ) assert input_json["settings"]["optimizer"]["enabled"] is False assert input_json["settings"]["optimizer"]["runs"] == 0 - assert input_json["sources"]["path"]["content"] != solc5source + assert input_json["sources"]["path.sol"]["content"] != solc5source def test_generate_input_json_evm(solc5source, monkeypatch): monkeypatch.setattr("solcx.get_solc_version", lambda: Version("0.5.5")) - fn = functools.partial(compiler.generate_input_json, {"path": solc5source}) + fn = functools.partial(compiler.generate_input_json, {"path.sol": solc5source}) assert fn()["settings"]["evmVersion"] == "petersburg" assert fn(evm_version="byzantium")["settings"]["evmVersion"] == "byzantium" assert fn(evm_version="petersburg")["settings"]["evmVersion"] == "petersburg" @@ -75,12 +75,12 @@ def test_generate_input_json_evm(solc5source, monkeypatch): def test_compile_input_json(solc5json): - assert "Foo" in solc5json["contracts"]["path"] - assert "Bar" in solc5json["contracts"]["path"] + assert "Foo" in solc5json["contracts"]["path.sol"] + assert "Bar" in solc5json["contracts"]["path.sol"] def test_compile_input_json_raises(): - input_json = compiler.generate_input_json({"path": "potato"}, True, 200) + input_json = compiler.generate_input_json({"path.sol": "potato"}, True, 200) with pytest.raises(CompilerError): compiler.compile_from_input_json(input_json) @@ -108,29 +108,29 @@ def _test_compiler(a, **kwargs): def test_build_json_keys(solc5source): - build_json = compiler.compile_and_format({"path": solc5source}) + build_json = compiler.compile_and_format({"path.sol": solc5source}) assert set(build.BUILD_KEYS) == set(build_json["Foo"]) def test_build_json_unlinked_libraries(solc4source, solc5source): - build_json = compiler.compile_and_format({"path": solc5source}, solc_version="0.5.7") + build_json = compiler.compile_and_format({"path.sol": solc5source}, solc_version="0.5.7") assert "__Bar__" in build_json["Foo"]["bytecode"] - build_json = compiler.compile_and_format({"path": solc4source}, solc_version="0.4.25") + build_json = compiler.compile_and_format({"path.sol": solc4source}, solc_version="0.4.25") assert "__Bar__" in build_json["Foo"]["bytecode"] def test_format_link_references(solc4json, solc5json): - evm = solc5json["contracts"]["path"]["Foo"]["evm"] + evm = solc5json["contracts"]["path.sol"]["Foo"]["evm"] assert "__Bar__" in compiler.solidity._format_link_references(evm) - evm = solc4json["contracts"]["path"]["Foo"]["evm"] + evm = solc4json["contracts"]["path.sol"]["Foo"]["evm"] assert "__Bar__" in compiler.solidity._format_link_references(evm) def test_compiler_errors(solc4source, solc5source): with pytest.raises(CompilerError): - compiler.compile_and_format({"path": solc4source}, solc_version="0.5.7") + compiler.compile_and_format({"path.sol": solc4source}, solc_version="0.5.7") with pytest.raises(CompilerError): - compiler.compile_and_format({"path": solc5source}, solc_version="0.4.25") + compiler.compile_and_format({"path.sol": solc5source}, solc_version="0.4.25") def test_min_version(): @@ -147,7 +147,7 @@ def test_find_solc_versions(find_version, msolc): assert "0.5.7" in find_version(">0.4.8 <0.5.8 || 0.5.11") assert "0.4.22" in find_version("0.5.9 || 0.4.22") with pytest.raises(PragmaError): - compiler.find_solc_versions({"Foo": "contract Foo {}"}) + compiler.find_solc_versions({"Foo.sol": "contract Foo {}"}) with pytest.raises(IncompatibleSolcVersion): find_version("^1.0.0", install_needed=False) with pytest.raises(IncompatibleSolcVersion): From 1a303f56e402d989bebfba757c2f7aa7bbe63ab4 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 12:32:17 +0400 Subject: [PATCH 15/39] accept vyper code in compile_source() --- brownie/project/main.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/brownie/project/main.py b/brownie/project/main.py index 1ace1e685..419758d40 100644 --- a/brownie/project/main.py +++ b/brownie/project/main.py @@ -398,7 +398,11 @@ def compile_source( "evm_version": evm_version, "minify_source": False, } - return TempProject("TempProject", {".sol": source}, compiler_config) + + if solc_version is not None or source.lstrip().startswith("pragma"): + return TempProject("TempSolcProject", {".sol": source}, compiler_config) + + return TempProject("TempVyperProject", {".vy": source}, compiler_config) def load(project_path: Union[Path, str, None] = None, name: Optional[str] = None) -> "Project": From b11415f35b0f0dedba4b94a28ea4d17014723600 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 13:47:38 +0400 Subject: [PATCH 16/39] only recompile vyper contracts when they have changed --- brownie/project/sources.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/brownie/project/sources.py b/brownie/project/sources.py index c166d1355..2432b9e1e 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -26,11 +26,7 @@ def __init__(self, contract_sources: Dict) -> None: self._source: Dict = {} self._contracts: Dict = {} for path, source in contract_sources.items(): - if path.endswith(".vy"): - # TODO minify vyper contracts - data = {path: {"offsets": [(0, 0)], "offset_map": [(0, 0)]}} - else: - data = _get_contract_data(source) + data = _get_contract_data(source, path) for name, values in data.items(): if name in self._contracts: raise NamespaceCollision(f"Project has multiple contracts named '{name}'") @@ -98,7 +94,7 @@ def get_hash(source: str, contract_name: str, minified: bool) -> str: if minified: source = minify(source)[0] try: - data = _get_contract_data(source)[contract_name] + data = _get_contract_data(source, "")[contract_name] offset = slice(*data["offset"]) return sha1(source[offset].encode()).hexdigest() except KeyError: @@ -147,10 +143,16 @@ def highlight_source(source: str, offset: Tuple, pad: int = 3) -> Tuple: return final, ln -def _get_contract_data(full_source: str) -> Dict: +def _get_contract_data(full_source: str, path_str: str) -> Dict: key = sha1(full_source.encode()).hexdigest() if key in _contract_data: return _contract_data[key] + + path = Path(path_str) + if path.suffix == ".vy": + _contract_data[key] = {path.stem: {"offset": (0, len(full_source)), "offset_map": []}} + return _contract_data[key] + minified_source, offset_map = minify(full_source) minified_key = sha1(minified_source.encode()).hexdigest() if minified_key in _contract_data: From 39d44842bdf494c476d18da8256dd9fa62019ddf Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 14:42:16 +0400 Subject: [PATCH 17/39] get vyper source dependencies during compilation --- brownie/project/compiler/vyper.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index ec9f6904e..d556db29a 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -34,7 +34,7 @@ def compile_from_input_json( if not silent: print("Compiling contracts...") print(f" Vyper version: {get_version()}") - return vyper_json.compile_json(input_json, allow_paths) + return vyper_json.compile_json(input_json, root_path=allow_paths) def _get_unique_build_json( @@ -52,13 +52,21 @@ def _get_unique_build_json( "bytecode": output_evm["bytecode"]["object"], "bytecodeSha1": sha1(output_evm["bytecode"]["object"].encode()).hexdigest(), "coverageMap": {"statements": statement_map, "branches": branch_map}, - "dependencies": [], + "dependencies": _get_dependencies(ast_json), "offset": pc_map[0]["offset"], "pcMap": pc_map, "type": "contract", } +def _get_dependencies(ast_json: List) -> List: + import_nodes = [i for i in ast_json if i["ast_type"] == "Import"] + import_nodes += [ + i for i in ast_json if i["ast_type"] == "ImportFrom" if i["module"] != "vyper.interfaces" + ] + return sorted(set([i["names"][0]["name"].split(".")[-1] for i in import_nodes])) + + def _generate_coverage_data( source_map_str: str, opcodes_str: str, source_str: str, contract_name: str, ast_json: List ) -> Tuple: From 9a2f42956c2b7d917940855117d1337513fad27b Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 15:51:13 +0400 Subject: [PATCH 18/39] minify vyper contracts --- brownie/project/compiler/__init__.py | 12 ++++-- brownie/project/main.py | 4 +- brownie/project/sources.py | 57 +++++++++++++++++----------- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index e664be2c1..44d75e71f 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -81,14 +81,15 @@ def compile_and_format( else: compiler_targets[solc_version] = list(solc_sources) + compiler_data: Dict = {"minify_source": minify} for version, path_list in compiler_targets.items(): if version == "vyper": language = "Vyper" - compiler_data = {"minify_source": False, "version": str(vyper.get_version())} + compiler_data["version"] = str(vyper.get_version()) else: set_solc_version(version) language = "Solidity" - compiler_data = {"minify_source": minify, "version": str(solidity.get_version())} + compiler_data["version"] = str(solidity.get_version()) to_compile = dict((k, v) for k, v in contract_sources.items() if k in path_list) @@ -137,7 +138,8 @@ def generate_input_json( if language == "Vyper": input_json["outputSelection"] = input_json["settings"].pop("outputSelection") input_json["sources"] = dict( - (k, {"content": sources.minify(v)[0] if minify else v}) for k, v in contract_sources.items() + (k, {"content": sources.minify(v, language)[0] if minify else v}) + for k, v in contract_sources.items() ) return input_json @@ -202,7 +204,9 @@ def generate_build_json( abi = output_json["contracts"][path][contract_name]["abi"] output_evm = output_json["contracts"][path][contract_name]["evm"] - hash_ = sources.get_hash(input_json["sources"][path]["content"], contract_name, minified) + hash_ = sources.get_hash( + input_json["sources"][path]["content"], contract_name, minified, input_json["language"] + ) if input_json["language"] == "Solidity": contract_node = next(i[contract_name] for i in source_nodes if i.absolutePath == path) diff --git a/brownie/project/main.py b/brownie/project/main.py index 419758d40..36f0203a3 100644 --- a/brownie/project/main.py +++ b/brownie/project/main.py @@ -187,7 +187,9 @@ def _compare_build_json(self, contract_name: str) -> bool: build_json = self._build.get(contract_name) except KeyError: return True - if build_json["sha1"] != get_hash(source, contract_name, config["minify_source"]): + if build_json["sha1"] != get_hash( + source, contract_name, config["minify_source"], build_json["language"] + ): return True return next( (True for k, v in build_json["compiler"].items() if config[k] and v != config[k]), False diff --git a/brownie/project/sources.py b/brownie/project/sources.py index 2432b9e1e..ce8020607 100644 --- a/brownie/project/sources.py +++ b/brownie/project/sources.py @@ -9,12 +9,19 @@ from brownie.exceptions import NamespaceCollision from brownie.utils import color -MINIFY_REGEX_PATTERNS = [ +SOLIDITY_MINIFY_REGEX = [ r"(?:\s*\/\/[^\n]*)|(?:\/\*[\s\S]*?\*\/)", # comments r"(?<=\n)\s+|[ \t]+(?=\n)", # leading / trailing whitespace r"(?<=[^\w\s])[ \t]+(?=\w)|(?<=\w)[ \t]+(?=[^\w\s])", # whitespace between expressions ] +VYPER_MINIFY_REGEX = [ + r"((\n|^)[\s]*?#[\s\S]*?)(?=\n[^#])", + r'([\s]*?"""[\s\S]*?""")(?=\n)', + r"([\s]*?'''[\s\S]*?''')(?=\n)", + r"(\n)(?=\n)", +] + _contract_data: Dict = {} @@ -67,10 +74,13 @@ def expand_offset(self, contract_name: str, offset: Tuple) -> Tuple: ) -def minify(source: str) -> Tuple[str, List]: +def minify(source: str, language: str = "Solidity") -> Tuple[str, List]: """Given contract source as a string, returns a minified version and an offset map.""" offsets = [(0, 0)] - pattern = f"({'|'.join(MINIFY_REGEX_PATTERNS)})" + if language == "Solidity": + pattern = f"({'|'.join(SOLIDITY_MINIFY_REGEX)})" + else: + pattern = f"({'|'.join(VYPER_MINIFY_REGEX)})" for match in re.finditer(pattern, source): offsets.append( (match.start() - offsets[-1][1], match.end() - match.start() + offsets[-1][1]) @@ -89,10 +99,10 @@ def is_inside_offset(inner: Tuple, outer: Tuple) -> bool: return outer[0] <= inner[0] <= inner[1] <= outer[1] -def get_hash(source: str, contract_name: str, minified: bool) -> str: +def get_hash(source: str, contract_name: str, minified: bool, language: str) -> str: """Returns a hash of the contract source code.""" if minified: - source = minify(source)[0] + source = minify(source, language)[0] try: data = _get_contract_data(source, "")[contract_name] offset = slice(*data["offset"]) @@ -149,30 +159,31 @@ def _get_contract_data(full_source: str, path_str: str) -> Dict: return _contract_data[key] path = Path(path_str) - if path.suffix == ".vy": - _contract_data[key] = {path.stem: {"offset": (0, len(full_source)), "offset_map": []}} - return _contract_data[key] + language = "Vyper" if path.suffix == ".vy" else "Solidity" - minified_source, offset_map = minify(full_source) + minified_source, offset_map = minify(full_source, language) minified_key = sha1(minified_source.encode()).hexdigest() if minified_key in _contract_data: return _contract_data[minified_key] - contracts = re.findall( - r"((?:contract|library|interface)\s[^;{]*{[\s\S]*?})\s*(?=(?:contract|library|interface)\s|$)", # NOQA: E501 - minified_source, - ) - data = {} - for source in contracts: - type_, name, inherited = re.findall( - r"(contract|library|interface)\s+(\S*)\s*(?:is\s+([\s\S]*?)|)(?:{)", source - )[0] - idx = minified_source.index(source) - offset = ( - idx + next(i[1] for i in offset_map if i[0] <= idx), - idx + len(source) + next(i[1] for i in offset_map if i[0] <= idx + len(source)), + if language == "Vyper": + data = {path.stem: {"offset": (0, len(full_source)), "offset_map": offset_map}} + else: + contracts = re.findall( + r"((?:contract|library|interface)\s[^;{]*{[\s\S]*?})\s*(?=(?:contract|library|interface)\s|$)", # NOQA: E501 + minified_source, ) - data[name] = {"offset_map": offset_map, "offset": offset} + data = {} + for source in contracts: + type_, name, inherited = re.findall( + r"(contract|library|interface)\s+(\S*)\s*(?:is\s+([\s\S]*?)|)(?:{)", source + )[0] + idx = minified_source.index(source) + offset = ( + idx + next(i[1] for i in offset_map if i[0] <= idx), + idx + len(source) + next(i[1] for i in offset_map if i[0] <= idx + len(source)), + ) + data[name] = {"offset_map": offset_map, "offset": offset} _contract_data[key] = data _contract_data[minified_key] = data return data From c328c2a593ce0fb39d2f43f10300956f0bea311f Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 21:50:03 +0400 Subject: [PATCH 19/39] expand compiler tests cases --- .../test_solidity.py} | 29 +++++-- tests/project/compiler/test_vyper.py | 78 +++++++++++++++++++ tests/project/conftest.py | 11 ++- 3 files changed, 109 insertions(+), 9 deletions(-) rename tests/project/{test_compiler.py => compiler/test_solidity.py} (86%) create mode 100644 tests/project/compiler/test_vyper.py diff --git a/tests/project/test_compiler.py b/tests/project/compiler/test_solidity.py similarity index 86% rename from tests/project/test_compiler.py rename to tests/project/compiler/test_solidity.py index c095ba6ac..4702a6403 100644 --- a/tests/project/test_compiler.py +++ b/tests/project/compiler/test_solidity.py @@ -24,6 +24,13 @@ def solc5json(solc5source): yield compiler.compile_from_input_json(input_json) +@pytest.fixture +def solc6json(solc6source): + compiler.set_solc_version("0.6.0") + input_json = compiler.generate_input_json({"path.sol": solc6source}, True, 200) + yield compiler.compile_from_input_json(input_json) + + @pytest.fixture def find_version(): source = """pragma solidity{};contract Foo {{}}""" @@ -44,9 +51,11 @@ def msolc(monkeypatch): def test_set_solc_version(): compiler.set_solc_version("0.5.7") - assert "0.5.7" in solcx.get_solc_version_string() + assert solcx.get_solc_version() == compiler.solidity.get_version() + assert solcx.get_solc_version().truncate() == Version("0.5.7") compiler.set_solc_version("0.4.25") - assert "0.4.25" in solcx.get_solc_version_string() + assert solcx.get_solc_version() == compiler.solidity.get_version() + assert solcx.get_solc_version().truncate() == Version("0.4.25") def test_generate_input_json(solc5source): @@ -112,17 +121,21 @@ def test_build_json_keys(solc5source): assert set(build.BUILD_KEYS) == set(build_json["Foo"]) -def test_build_json_unlinked_libraries(solc4source, solc5source): +def test_build_json_unlinked_libraries(solc4source, solc5source, solc6source): + build_json = compiler.compile_and_format({"path.sol": solc4source}, solc_version="0.4.25") + assert "__Bar__" in build_json["Foo"]["bytecode"] build_json = compiler.compile_and_format({"path.sol": solc5source}, solc_version="0.5.7") assert "__Bar__" in build_json["Foo"]["bytecode"] - build_json = compiler.compile_and_format({"path.sol": solc4source}, solc_version="0.4.25") + build_json = compiler.compile_and_format({"path.sol": solc6source}, solc_version="0.6.0") assert "__Bar__" in build_json["Foo"]["bytecode"] -def test_format_link_references(solc4json, solc5json): +def test_format_link_references(solc4json, solc5json, solc6json): + evm = solc4json["contracts"]["path.sol"]["Foo"]["evm"] + assert "__Bar__" in compiler.solidity._format_link_references(evm) evm = solc5json["contracts"]["path.sol"]["Foo"]["evm"] assert "__Bar__" in compiler.solidity._format_link_references(evm) - evm = solc4json["contracts"]["path.sol"]["Foo"]["evm"] + evm = solc6json["contracts"]["path.sol"]["Foo"]["evm"] assert "__Bar__" in compiler.solidity._format_link_references(evm) @@ -180,3 +193,7 @@ def test_first_revert(BrownieTester, ExternalCallTester): assert next((i for i in pc_map.values() if "first_revert" in i), False) pc_map = BrownieTester._build["pcMap"] assert not next((i for i in pc_map.values() if "first_revert" in i), False) + + +def test_compile_empty(): + compiler.compile_and_format({"empty.sol": ""}, solc_version="0.4.25") diff --git a/tests/project/compiler/test_vyper.py b/tests/project/compiler/test_vyper.py new file mode 100644 index 000000000..a8aa6caa0 --- /dev/null +++ b/tests/project/compiler/test_vyper.py @@ -0,0 +1,78 @@ +#!/usr/bin/python3 + +import functools + +import pytest +import vyper +from semantic_version import Version +from vyper.exceptions import StructureException + +from brownie.project import build, compiler + + +@pytest.fixture +def vysource(): + return """ +# comments are totally kickass +@public +def test() -> bool: + return True +""" + + +@pytest.fixture +def vyjson(vysource): + input_json = compiler.generate_input_json({"path.vy": vysource}, language="Vyper") + yield compiler.compile_from_input_json(input_json) + + +def test_version(): + assert compiler.vyper.get_version() == Version.coerce(vyper.__version__) + + +def test_generate_input_json(vysource): + input_json = compiler.generate_input_json({"path.vy": vysource}, minify=False, language="Vyper") + assert "optimizer" not in input_json["settings"] + assert input_json["sources"]["path.vy"]["content"] == vysource + input_json = compiler.generate_input_json({"path.vy": vysource}, minify=True, language="Vyper") + assert "optimizer" not in input_json["settings"] + assert input_json["sources"]["path.vy"]["content"] != vysource + + +def test_generate_input_json_evm(vysource): + fn = functools.partial(compiler.generate_input_json, {"path.vy": vysource}, language="Vyper") + assert fn()["settings"]["evmVersion"] == "byzantium" + assert fn(evm_version="byzantium")["settings"]["evmVersion"] == "byzantium" + assert fn(evm_version="petersburg")["settings"]["evmVersion"] == "petersburg" + + +def test_compile_input_json(vyjson): + assert "path" in vyjson["contracts"]["path.vy"] + + +def test_compile_input_json_raises(): + input_json = compiler.generate_input_json({"path.vy": "potato"}, language="Vyper") + with pytest.raises(StructureException): + compiler.compile_from_input_json(input_json) + + +def test_build_json_keys(vysource): + build_json = compiler.compile_and_format({"path.vy": vysource}) + assert set(build.BUILD_KEYS) == set(build_json["path"]) + + +def test_dependencies(vysource): + code = """ +import path as foo +from vyper.interfaces import ERC20 +from foo import bar +""" + + build_json = compiler.compile_and_format( + {"path.vy": vysource, "deps.vy": code, "foo/bar.vy": vysource} + ) + assert build_json["deps"]["dependencies"] == ["bar", "path"] + + +def test_compile_empty(): + compiler.compile_and_format({"empty.vy": ""}) diff --git a/tests/project/conftest.py b/tests/project/conftest.py index 5277a1b19..0cec45af2 100644 --- a/tests/project/conftest.py +++ b/tests/project/conftest.py @@ -3,7 +3,7 @@ import pytest test_source = """ -pragma solidity ^0.5.0; +pragma solidity [VERSION]; library Bar { function baz(uint a, uint b) external pure returns (uint) { @@ -29,13 +29,18 @@ def btsource(testproject): return fs.read() +@pytest.fixture +def solc6source(): + return test_source.replace("[VERSION]", "^0.6.0") + + @pytest.fixture def solc5source(): - return test_source + return test_source.replace("[VERSION]", "^0.5.0") @pytest.fixture def solc4source(): source = test_source.replace("payable ", "") - source = source.replace("^0.5.0", "^0.4.25") + source = source.replace("[VERSION]", "^0.4.25") return source From 00af8b5c28c718f368ca4b0bab98923a168d8b02 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Mon, 23 Dec 2019 21:56:27 +0400 Subject: [PATCH 20/39] minor bugfixes and formatting --- brownie/project/compiler/__init__.py | 52 +++++++++++++++++----------- brownie/project/compiler/vyper.py | 8 ++--- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index 44d75e71f..9b5ee5055 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -22,7 +22,6 @@ "outputSelection": { "*": {"*": ["abi", "evm.bytecode", "evm.deployedBytecode"], "": ["ast"]} }, - "optimizer": {"enabled": True, "runs": 200}, "evmVersion": None, "remappings": [], }, @@ -132,10 +131,10 @@ def generate_input_json( input_json: Dict = deepcopy(STANDARD_JSON) input_json["language"] = language - input_json["settings"]["optimizer"]["enabled"] = optimize - input_json["settings"]["optimizer"]["runs"] = runs if optimize else 0 input_json["settings"]["evmVersion"] = evm_version - if language == "Vyper": + if language == "Solidity": + input_json["settings"]["optimizer"] = {"enabled": optimize, "runs": runs if optimize else 0} + elif language == "Vyper": input_json["outputSelection"] = input_json["settings"].pop("outputSelection") input_json["sources"] = dict( (k, {"content": sources.minify(v, language)[0] if minify else v}) @@ -183,33 +182,40 @@ def generate_build_json( if compiler_data is None: compiler_data = {} - compiler_data.update( - { - "optimize": input_json["settings"]["optimizer"]["enabled"], - "runs": input_json["settings"]["optimizer"]["runs"], - "evm_version": input_json["settings"]["evmVersion"], - } - ) - minified = "minify_source" in compiler_data and compiler_data["minify_source"] + compiler_data["evm_version"] = input_json["settings"]["evmVersion"] + minified = compiler_data.get("minify_source", False) build_json = {} path_list = list(input_json["sources"]) if input_json["language"] == "Solidity": + compiler_data.update( + { + "optimize": input_json["settings"]["optimizer"]["enabled"], + "runs": input_json["settings"]["optimizer"]["runs"], + } + ) source_nodes, statement_nodes, branch_nodes = solidity._get_nodes(output_json) - for path, contract_name in [(k, v) for k in path_list for v in output_json["contracts"][k]]: + for path_str, contract_name in [ + (k, v) for k in path_list for v in output_json["contracts"].get(k, {}) + ]: if not silent: print(f" - {contract_name}...") - abi = output_json["contracts"][path][contract_name]["abi"] - output_evm = output_json["contracts"][path][contract_name]["evm"] + abi = output_json["contracts"][path_str][contract_name]["abi"] + output_evm = output_json["contracts"][path_str][contract_name]["evm"] hash_ = sources.get_hash( - input_json["sources"][path]["content"], contract_name, minified, input_json["language"] + input_json["sources"][path_str]["content"], + contract_name, + minified, + input_json["language"], ) if input_json["language"] == "Solidity": - contract_node = next(i[contract_name] for i in source_nodes if i.absolutePath == path) + contract_node = next( + i[contract_name] for i in source_nodes if i.absolutePath == path_str + ) build_json[contract_name] = solidity._get_unique_build_json( output_evm, contract_node, @@ -220,13 +226,17 @@ def generate_build_json( else: build_json[contract_name] = vyper._get_unique_build_json( - output_evm, path, contract_name, output_json["sources"][path]["ast"] + output_evm, + path_str, + contract_name, + output_json["sources"][path_str]["ast"], + (0, len(input_json["sources"][path_str]["content"])), ) build_json[contract_name].update( { "abi": abi, - "ast": output_json["sources"][path]["ast"], + "ast": output_json["sources"][path_str]["ast"], "compiler": compiler_data, "contractName": contract_name, "deployedBytecode": output_evm["deployedBytecode"]["object"], @@ -234,9 +244,9 @@ def generate_build_json( "language": input_json["language"], "opcodes": output_evm["deployedBytecode"]["opcodes"], "sha1": hash_, - "source": input_json["sources"][path]["content"], + "source": input_json["sources"][path_str]["content"], "sourceMap": output_evm["bytecode"].get("sourceMap", ""), - "sourcePath": path, + "sourcePath": path_str, } ) diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index d556db29a..0c166255e 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -38,22 +38,22 @@ def compile_from_input_json( def _get_unique_build_json( - output_evm: Dict, source_str: str, contract_name: str, ast_json: List + output_evm: Dict, path_str: str, contract_name: str, ast_json: List, offset: Tuple ) -> Dict: pc_map, statement_map, branch_map = _generate_coverage_data( output_evm["deployedBytecode"]["sourceMap"], output_evm["deployedBytecode"]["opcodes"], - source_str, + path_str, contract_name, ast_json, ) return { - "allSourcePaths": [source_str], + "allSourcePaths": [path_str], "bytecode": output_evm["bytecode"]["object"], "bytecodeSha1": sha1(output_evm["bytecode"]["object"].encode()).hexdigest(), "coverageMap": {"statements": statement_map, "branches": branch_map}, "dependencies": _get_dependencies(ast_json), - "offset": pc_map[0]["offset"], + "offset": offset, "pcMap": pc_map, "type": "contract", } From 972fddd1bc982d670005dd5e1680e69321bc90c5 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 24 Dec 2019 10:42:41 +0400 Subject: [PATCH 21/39] correctly save minified source offsets in build artifacts --- CHANGELOG.md | 2 ++ brownie/project/main.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a225546..cc9b005ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This changelog format is based on [Keep a Changelog](https://keepachangelog.com/ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/iamdefinitelyahuman/brownie) +### Fixed +- Correctly save minified source offsets in build artifacts ## [1.3.0](https://github.com/iamdefinitelyahuman/brownie/tree/v1.3.0) - 2019-12-20 ### Added diff --git a/brownie/project/main.py b/brownie/project/main.py index 36f0203a3..d73f199a3 100644 --- a/brownie/project/main.py +++ b/brownie/project/main.py @@ -64,11 +64,11 @@ def _compile(self, sources: Dict, compiler_config: Dict, silent: bool) -> None: allow_paths=allow_paths, ) for data in build_json.values(): - self._build._add(data) if self._path is not None: path = self._path.joinpath(f"build/contracts/{data['contractName']}.json") with path.open("w") as fp: json.dump(data, fp, sort_keys=True, indent=2, default=sorted) + self._build._add(data) def _create_containers(self) -> None: # create container objects From 66f8fdfb50baf61e8c6505ecc1999f539c40df75 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 24 Dec 2019 11:11:13 +0400 Subject: [PATCH 22/39] add UnsupportedLanguage exception --- brownie/exceptions.py | 4 ++++ brownie/project/compiler/__init__.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/brownie/exceptions.py b/brownie/exceptions.py index c271b4b6e..0a97a4637 100644 --- a/brownie/exceptions.py +++ b/brownie/exceptions.py @@ -117,3 +117,7 @@ class PragmaError(Exception): class InvalidManifest(Exception): pass + + +class UnsupportedLanguage(Exception): + pass diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index 9b5ee5055..e182efb40 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -5,6 +5,7 @@ from semantic_version import Version +from brownie.exceptions import UnsupportedLanguage from brownie.project import sources from brownie.project.compiler.solidity import ( # NOQA: F401 find_best_solc_version, @@ -62,7 +63,7 @@ def compile_and_format( return {} if [i for i in contract_sources if not i.endswith(".sol") and not i.endswith(".vy")]: - raise ValueError("Source filenames must end in .sol or .vy") + raise UnsupportedLanguage("Source filenames must end in .sol or .vy") build_json: Dict = {} compiler_targets = {} @@ -121,7 +122,7 @@ def generate_input_json( """ if language not in ("Solidity", "Vyper"): - raise ValueError(f"Unsupported language: {language}") + raise UnsupportedLanguage(f"{language}") if evm_version is None: if language == "Solidity": @@ -134,7 +135,7 @@ def generate_input_json( input_json["settings"]["evmVersion"] = evm_version if language == "Solidity": input_json["settings"]["optimizer"] = {"enabled": optimize, "runs": runs if optimize else 0} - elif language == "Vyper": + else: input_json["outputSelection"] = input_json["settings"].pop("outputSelection") input_json["sources"] = dict( (k, {"content": sources.minify(v, language)[0] if minify else v}) @@ -162,7 +163,7 @@ def compile_from_input_json( return vyper.compile_from_input_json(input_json, silent, allow_paths) if input_json["language"] == "Solidity": return solidity.compile_from_input_json(input_json, silent, allow_paths) - raise ValueError(f"Unsupported language: {input_json['language']}") + raise UnsupportedLanguage(f"{input_json['language']}") def generate_build_json( @@ -177,6 +178,10 @@ def generate_build_json( silent: verbose reporting Returns: build json dict""" + + if input_json["language"] not in ("Solidity", "Vyper"): + raise UnsupportedLanguage(f"{input_json['language']}") + if not silent: print("Generating build data...") From 48a4f00f2f6e71293b961273545104ebf4981fc9 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 24 Dec 2019 11:12:18 +0400 Subject: [PATCH 23/39] update tests, minor bugfixes --- brownie/project/compiler/solidity.py | 2 +- tests/project/compiler/test_main_compiler.py | 30 ++++++++++++++++++++ tests/project/compiler/test_vyper.py | 10 ------- tests/project/conftest.py | 10 +++++++ 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 tests/project/compiler/test_main_compiler.py diff --git a/brownie/project/compiler/solidity.py b/brownie/project/compiler/solidity.py index a17dc5ef3..62463188b 100644 --- a/brownie/project/compiler/solidity.py +++ b/brownie/project/compiler/solidity.py @@ -141,7 +141,7 @@ def find_solc_versions( if not version or (install_latest and latest > version): to_install.add(latest) - elif latest > version: + elif latest and latest > version: new_versions.add(str(version)) # install new versions if needed diff --git a/tests/project/compiler/test_main_compiler.py b/tests/project/compiler/test_main_compiler.py new file mode 100644 index 000000000..8c56b10ed --- /dev/null +++ b/tests/project/compiler/test_main_compiler.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 + +import pytest + +from brownie.exceptions import UnsupportedLanguage +from brownie.project import compiler + + +def test_multiple_compilers(solc4source, vysource): + compiler.compile_and_format( + { + "solc4.sol": solc4source, + "vyper.vy": vysource, + "solc6.sol": "pragma solidity 0.6.0; contract Foo {}", + } + ) + + +def test_wrong_suffix(): + with pytest.raises(UnsupportedLanguage): + compiler.compile_and_format({"foo.bar": ""}) + + +def test_unknown_language(): + with pytest.raises(UnsupportedLanguage): + compiler.generate_input_json({"foo": ""}, language="Bar") + with pytest.raises(UnsupportedLanguage): + compiler.compile_from_input_json({"language": "FooBar"}) + with pytest.raises(UnsupportedLanguage): + compiler.generate_build_json({"language": "BarBaz"}, {}) diff --git a/tests/project/compiler/test_vyper.py b/tests/project/compiler/test_vyper.py index a8aa6caa0..385bea7e5 100644 --- a/tests/project/compiler/test_vyper.py +++ b/tests/project/compiler/test_vyper.py @@ -10,16 +10,6 @@ from brownie.project import build, compiler -@pytest.fixture -def vysource(): - return """ -# comments are totally kickass -@public -def test() -> bool: - return True -""" - - @pytest.fixture def vyjson(vysource): input_json = compiler.generate_input_json({"path.vy": vysource}, language="Vyper") diff --git a/tests/project/conftest.py b/tests/project/conftest.py index 0cec45af2..726c0545b 100644 --- a/tests/project/conftest.py +++ b/tests/project/conftest.py @@ -44,3 +44,13 @@ def solc4source(): source = test_source.replace("payable ", "") source = source.replace("[VERSION]", "^0.4.25") return source + + +@pytest.fixture +def vysource(): + return """ +# comments are totally kickass +@public +def test() -> bool: + return True +""" From 7b52543f61bdc67a14ae0a4dce3c56e9ebcaa5af Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 24 Dec 2019 11:12:37 +0400 Subject: [PATCH 24/39] add custom error message for vyper zero division --- brownie/project/compiler/vyper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index 0c166255e..80f35c420 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -140,6 +140,8 @@ def _generate_coverage_data( elif node["ast_type"] in ("AugAssign", "BinOp"): if node["op"]["ast_type"] == "Sub": pc_list[-1]["dev"] = "Integer underflow" + elif node["op"]["ast_type"] == "Div": + pc_list[-1]["dev"] = "Division by zero" else: pc_list[-1]["dev"] = "Integer overflow" continue From 6e2ea79648199572e614a1eb39b9180fb67a6c46 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 24 Dec 2019 11:55:46 +0400 Subject: [PATCH 25/39] more vyper test cases --- brownie/project/compiler/__init__.py | 2 + tests/conftest.py | 5 ++ .../contracts/VyperTester.vy | 46 +++++++++++++++++++ tests/network/transaction/test_revert_msg.py | 32 ++++++++++++- tests/project/test_main_project.py | 12 ++++- 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 tests/data/brownie-test-project/contracts/VyperTester.vy diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index e182efb40..992ad0c91 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -230,6 +230,8 @@ def generate_build_json( ) else: + if contract_name == "": + contract_name = "Vyper" build_json[contract_name] = vyper._get_unique_build_json( output_evm, path_str, diff --git a/tests/conftest.py b/tests/conftest.py index 64f38128f..9fb86f9fd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -271,6 +271,11 @@ def ext_tester(ExternalCallTester, accounts): return ExternalCallTester.deploy({"from": accounts[0]}) +@pytest.fixture +def vypertester(testproject, devnetwork, accounts): + return testproject.VyperTester.deploy({"from": accounts[0]}) + + # ipfs fixtures diff --git a/tests/data/brownie-test-project/contracts/VyperTester.vy b/tests/data/brownie-test-project/contracts/VyperTester.vy new file mode 100644 index 000000000..2b014419b --- /dev/null +++ b/tests/data/brownie-test-project/contracts/VyperTester.vy @@ -0,0 +1,46 @@ + +stuff: public(uint256[4]) + + +@public +def outOfBounds(i: uint256, value: uint256) -> bool: + self.stuff[i] = value + return True + + +@public +def overflow(a: uint256, b: uint256) -> uint256: + return a * b + + +@public +def underflow(a: uint256, b: uint256) -> uint256: + return a - b + + +@public +def zeroDivision(a: uint256, b: uint256) -> uint256: + return a / b + + +@public +def revertStrings(a: uint256) -> bool: + assert a != 0, "zero" + assert a != 1 # dev: one + assert a != 2, "two" # dev: error + assert a != 3 # error + assert_modifiable(a != 4) # dev: such modifiable, wow + if a != 31337: + return True + raise "awesome show" # dev: great job + + +@public +def branches(a: uint256, b: bool) -> bool: + if a > 4: + return True + elif b: + return False + if a - 2 == 3 and not b: + return True + return False diff --git a/tests/network/transaction/test_revert_msg.py b/tests/network/transaction/test_revert_msg.py index 5729d3040..f9215cc6e 100644 --- a/tests/network/transaction/test_revert_msg.py +++ b/tests/network/transaction/test_revert_msg.py @@ -10,7 +10,7 @@ def test_revert_msg_via_jump(ext_tester, console_mode): assert tx.revert_msg == "dev: should jump to a revert" -def test_revert_msg(evmtester, console_mode): +def test_solidity_revert_msg(evmtester, console_mode): tx = evmtester.revertStrings(0) assert tx.revert_msg == "zero" tx = evmtester.revertStrings(1) @@ -23,6 +23,21 @@ def test_revert_msg(evmtester, console_mode): assert tx.revert_msg == "dev: great job" +def test_vyper_revert_msg(vypertester, console_mode): + tx = vypertester.revertStrings(0) + assert tx.revert_msg == "zero" + tx = vypertester.revertStrings(1) + assert tx.revert_msg == "dev: one" + tx = vypertester.revertStrings(2) + assert tx.revert_msg == "two" + tx = vypertester.revertStrings(3) + assert tx.revert_msg == "" + tx = vypertester.revertStrings(4) + assert tx.revert_msg == "dev: such modifiable, wow" + tx = vypertester.revertStrings(31337) + assert tx.revert_msg == "awesome show" + + def test_nonpayable(tester, evmtester, console_mode): tx = evmtester.revertStrings(0, {"value": 100}) assert tx.revert_msg == "Cannot send ether to nonpayable function" @@ -30,7 +45,7 @@ def test_nonpayable(tester, evmtester, console_mode): assert tx.revert_msg == "Cannot send ether to nonpayable function" -def test_invalid_opcodes(evmtester): +def test_solidity_invalid_opcodes(evmtester): with pytest.raises(VirtualMachineError) as exc: evmtester.invalidOpcodes(0, 0) assert exc.value.revert_msg == "invalid opcode" @@ -40,3 +55,16 @@ def test_invalid_opcodes(evmtester): with pytest.raises(VirtualMachineError) as exc: evmtester.invalidOpcodes(3, 3) assert exc.value.revert_msg == "Index out of range" + + +def test_vyper_revert_reasons(vypertester, console_mode): + tx = vypertester.outOfBounds(6, 31337) + assert tx.revert_msg == "Index out of range" + tx = vypertester.overflow(6, 2 ** 255) + assert tx.revert_msg == "Integer overflow" + tx = vypertester.underflow(6, 8) + assert tx.revert_msg == "Integer underflow" + tx = vypertester.zeroDivision(6, 0) + assert tx.revert_msg == "Division by zero" + tx = vypertester.overflow(0, 0, {"value": 31337}) + assert tx.revert_msg == "Cannot send ether to nonpayable function" diff --git a/tests/project/test_main_project.py b/tests/project/test_main_project.py index 7abc59e5c..b06c31366 100644 --- a/tests/project/test_main_project.py +++ b/tests/project/test_main_project.py @@ -87,15 +87,25 @@ def test_close(project, testproject): testproject.close() -def test_compile_object(project, solc5source): +def test_compile_solc_object(project, solc5source): temp = project.compile_source(solc5source) assert type(temp) is TempProject assert isinstance(temp, _ProjectBase) assert len(temp) == 2 + assert temp._name == "TempSolcProject" assert "Foo" in temp assert "Bar" in temp +def test_compile_vyper_object(project): + temp = project.compile_source("@public\ndef x() -> bool: return True") + assert type(temp) is TempProject + assert isinstance(temp, _ProjectBase) + assert len(temp) == 1 + assert temp._name == "TempVyperProject" + assert "Vyper" in temp + + def test_compile_namespace(project, solc5source): project.compile_source(solc5source) assert not hasattr(project, "TempProject") From 17dba1d7bc2f1f38d212a4c1755fc8bfca0965f9 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 24 Dec 2019 12:20:30 +0400 Subject: [PATCH 26/39] improve vyper statement nodes --- brownie/project/compiler/vyper.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index 80f35c420..a2502818e 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -75,16 +75,10 @@ def _generate_coverage_data( source_map = deque(expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) - fn_offsets = dict( - (i["name"], _convert_src(i["src"])) for i in ast_json if i["ast_type"] == "FunctionDef" - ) - stmt_nodes = set( - _convert_src(x["src"]) - for i in ast_json - if i["ast_type"] == "FunctionDef" - for x in i["body"] - ) + fn_nodes = [i for i in ast_json if i["ast_type"] == "FunctionDef"] + fn_offsets = dict((i["name"], _convert_src(i["src"])) for i in fn_nodes) + stmt_nodes = set(_convert_src(i["src"]) for i in _get_statement_nodes(fn_nodes)) statement_map: Dict = {} branch_map: Dict = {} @@ -180,3 +174,14 @@ def _find_node_by_offset(ast_json: List, offset: Tuple) -> Dict: node_list = [i for i in node.values() if isinstance(i, dict) and "ast_type" in i] node_list.extend([x for i in node.values() if isinstance(i, list) for x in i]) return _find_node_by_offset(node_list, offset) + + +def _get_statement_nodes(ast_json: List) -> List: + stmt_nodes: List = [] + for node in ast_json: + children = [x for v in node.values() if isinstance(v, list) for x in v] + if children: + stmt_nodes += _get_statement_nodes(children) + else: + stmt_nodes.append(node) + return stmt_nodes From 4c8045e744c3eb3e85918b1dfa4d858b2ff21d85 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 25 Dec 2019 17:34:43 +0200 Subject: [PATCH 27/39] simplify branch coverage logic for vyper --- brownie/network/transaction.py | 13 ++++++++----- brownie/project/compiler/vyper.py | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/brownie/network/transaction.py b/brownie/network/transaction.py index c8e315c35..25516084d 100644 --- a/brownie/network/transaction.py +++ b/brownie/network/transaction.py @@ -440,7 +440,7 @@ def _expand_trace(self) -> None: continue # calculate coverage - if "active_branches" in last: + if last["coverage"]: if pc["path"] not in coverage_eval[last["name"]]: coverage_eval[last["name"]][pc["path"]] = [set(), set(), set()] if "statement" in pc: @@ -448,11 +448,12 @@ def _expand_trace(self) -> None: if "branch" in pc: if pc["op"] != "JUMPI": last["active_branches"].add(pc["branch"]) - elif pc["branch"] in last["active_branches"]: + elif "active_branches" not in last or pc["branch"] in last["active_branches"]: # false, true key = 1 if trace[i + 1]["pc"] == trace[i]["pc"] + 1 else 2 coverage_eval[last["name"]][pc["path"]][key].add(pc["branch"]) - last["active_branches"].remove(pc["branch"]) + if "active_branches" in last: + last["active_branches"].remove(pc["branch"]) # ignore jumps with no function - they are compiler optimizations if "jump" in pc: @@ -733,7 +734,7 @@ def _raise(msg: str, source: str) -> None: def _get_last_map(address: EthAddress, sig: str) -> Dict: contract = _find_contract(address) - last_map = {"address": EthAddress(address), "jumpDepth": 0, "name": None} + last_map = {"address": EthAddress(address), "jumpDepth": 0, "name": None, "coverage": False} if contract: last_map.update( contract=contract, @@ -741,7 +742,9 @@ def _get_last_map(address: EthAddress, sig: str) -> Dict: fn=[f"{contract._name}.{contract.get_method(sig)}"], ) if contract._build: - last_map.update(pc_map=contract._build["pcMap"], active_branches=set()) + last_map.update(pc_map=contract._build["pcMap"], coverage=True) + if contract._build["language"] == "Solidity": + last_map["active_branches"] = set() else: last_map.update(contract=None, fn=[f".{sig}"]) return last_map diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index a2502818e..ca01f0f5c 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -145,7 +145,6 @@ def _generate_coverage_data( ): # branch coverage pc_list[-1]["branch"] = count - pc_list[-2]["branch"] = count branch_map.setdefault(pc_list[-1]["fn"], {}) if node["ast_type"] == "If": branch_map[pc_list[-1]["fn"]][count] = _convert_src(node["test"]["src"]) + (False,) From acc17966d8dfe71f218d1734edab69b9894367fb Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 31 Dec 2019 18:45:55 +0200 Subject: [PATCH 28/39] add support for vyper fixed168x10 decimal type --- brownie/convert.py | 18 +++++++++++++++++- brownie/network/contract.py | 6 +++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/brownie/convert.py b/brownie/convert.py index d186e095f..b8ce3fc53 100644 --- a/brownie/convert.py +++ b/brownie/convert.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 from copy import deepcopy +from decimal import Decimal from typing import Any, Dict, ItemsView, KeysView, List, Tuple, TypeVar, Union import eth_utils @@ -101,7 +102,7 @@ def _return_int(original: Any, value: Any) -> int: try: return int(value) except ValueError: - raise TypeError(f"Could not convert {type(original)} '{original}' to wei.") + raise TypeError(f"Cannot convert {type(original)} '{original}' to wei.") def to_uint(value: Any, type_: str = "uint256") -> "Wei": @@ -129,6 +130,19 @@ def _check_int_size(type_: Any) -> int: return size +def to_decimal(value: Any) -> Decimal: + if isinstance(value, float): + raise TypeError("Cannot cast float to decimal") + if not isinstance(value, (Decimal, int, str)): + raise TypeError(f"Cannot convert {type(value)} '{value}' to decimal.") + d: Decimal = Decimal(value) + if d < -2 ** 127 or d >= 2 ** 127: + raise OverflowError(f"{value} is outside allowable range for decimal") + if d.quantize(Decimal("1.0000000000")) != d: + raise ValueError("Maximum of 10 decimal points allowed") + return d + + class EthAddress(str): """String subclass that raises TypeError when compared to a non-address.""" @@ -313,6 +327,8 @@ def _format_single(type_: str, value: Any) -> Any: return to_uint(value, type_) elif "int" in type_: return to_int(value, type_) + elif type_ == "fixed168x10": + return to_decimal(value) elif type_ == "bool": return to_bool(value) elif type_ == "address": diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 3cbd6f2da..536104dd7 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -374,7 +374,11 @@ def __init__(self, address: str, abi: Dict, name: str, owner: Optional[AccountsT self.signature = _signature(abi) def __repr__(self) -> str: - pay = "payable " if self.abi["stateMutability"] == "payable" else "" + if "payable" in self.abi: + pay_bool = self.abi["payable"] + else: + pay_bool = self.abi["stateMutability"] == "payable" + pay = "payable " if pay_bool else "" return f"<{type(self).__name__} {pay}object '{self.abi['name']}({_inputs(self.abi)})'>" def call(self, *args: Tuple) -> Any: From 27430582e22f9857f5f6566b6b66da70633819fc Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 31 Dec 2019 22:26:31 +0200 Subject: [PATCH 29/39] show fixed168x10 as decimal in console --- brownie/network/contract.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 536104dd7..504e85749 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -530,18 +530,21 @@ def _get_method_object( return ContractTx(address, abi, name, owner) -def _params(abi_params: List) -> List: +def _params(abi_params: List, substitutions: Optional[Dict] = None) -> List: types = [] + if substitutions is None: + substitutions = {} for i in abi_params: if i["type"] != "tuple": - types.append((i["name"], i["type"])) + types.append((i["name"], substitutions.get(i["type"], i["type"]))) continue - types.append((i["name"], f"({','.join(x[1] for x in _params(i['components']))})")) + params = [i[1] for i in _params(i["components"], substitutions)] + types.append((i["name"], f"({','.join(params)})")) return types def _inputs(abi: Dict) -> str: - params = _params(abi["inputs"]) + params = _params(abi["inputs"], {"fixed168x10": "decimal"}) return ", ".join(f"{i[1]}{' '+i[0] if i[0] else ''}" for i in params) From c27b859c74e0a497489960b05cc7ecfbbbfcc81d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 31 Dec 2019 23:22:45 +0200 Subject: [PATCH 30/39] add Decimal to console --- brownie/_cli/console.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brownie/_cli/console.py b/brownie/_cli/console.py index cc4d40475..8b06f95b0 100644 --- a/brownie/_cli/console.py +++ b/brownie/_cli/console.py @@ -4,6 +4,7 @@ import code import importlib import sys +from decimal import Decimal from docopt import docopt @@ -57,6 +58,7 @@ def __init__(self, project=None): try: Gui = importlib.import_module("brownie._gui").Gui locals_dict["Gui"] = Gui + locals_dict["Decimal"] = Decimal except ModuleNotFoundError: pass From 4eb55db029af6b236d93134110e8ef845562f40c Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 31 Dec 2019 23:23:12 +0200 Subject: [PATCH 31/39] add custom revert string for vyper modulo by 0 --- brownie/project/compiler/vyper.py | 2 + .../contracts/VyperTester.vy | 51 +++++++++++-------- tests/network/transaction/test_revert_msg.py | 2 + 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/brownie/project/compiler/vyper.py b/brownie/project/compiler/vyper.py index ca01f0f5c..74f72e587 100644 --- a/brownie/project/compiler/vyper.py +++ b/brownie/project/compiler/vyper.py @@ -136,6 +136,8 @@ def _generate_coverage_data( pc_list[-1]["dev"] = "Integer underflow" elif node["op"]["ast_type"] == "Div": pc_list[-1]["dev"] = "Division by zero" + elif node["op"]["ast_type"] == "Mod": + pc_list[-1]["dev"] = "Modulo by zero" else: pc_list[-1]["dev"] = "Integer overflow" continue diff --git a/tests/data/brownie-test-project/contracts/VyperTester.vy b/tests/data/brownie-test-project/contracts/VyperTester.vy index 2b014419b..c1780a7e0 100644 --- a/tests/data/brownie-test-project/contracts/VyperTester.vy +++ b/tests/data/brownie-test-project/contracts/VyperTester.vy @@ -2,6 +2,35 @@ stuff: public(uint256[4]) +@public +def branches(a: uint256, b: bool) -> bool: + if a > 4: + return True + elif b: + return False + if a - 2 == 3 and not b: + return True + return False + + +@public +def revertStrings(a: uint256) -> bool: + assert a != 0, "zero" + assert a != 1 # dev: one + assert a != 2, "two" # dev: error + assert a != 3 # error + assert_modifiable(a != 4) # dev: such modifiable, wow + if a != 31337: + return True + raise "awesome show" # dev: great job + + +@public +@constant +def fixedType(a: decimal, b: decimal[2]) -> decimal[3]: + return [a, b[0], b[1]] + + @public def outOfBounds(i: uint256, value: uint256) -> bool: self.stuff[i] = value @@ -24,23 +53,5 @@ def zeroDivision(a: uint256, b: uint256) -> uint256: @public -def revertStrings(a: uint256) -> bool: - assert a != 0, "zero" - assert a != 1 # dev: one - assert a != 2, "two" # dev: error - assert a != 3 # error - assert_modifiable(a != 4) # dev: such modifiable, wow - if a != 31337: - return True - raise "awesome show" # dev: great job - - -@public -def branches(a: uint256, b: bool) -> bool: - if a > 4: - return True - elif b: - return False - if a - 2 == 3 and not b: - return True - return False +def zeroModulo(a: uint256, b: uint256) -> uint256: + return a % b diff --git a/tests/network/transaction/test_revert_msg.py b/tests/network/transaction/test_revert_msg.py index ea8045e31..d4cec9e53 100644 --- a/tests/network/transaction/test_revert_msg.py +++ b/tests/network/transaction/test_revert_msg.py @@ -69,5 +69,7 @@ def test_vyper_revert_reasons(vypertester, console_mode): assert tx.revert_msg == "Integer underflow" tx = vypertester.zeroDivision(6, 0) assert tx.revert_msg == "Division by zero" + tx = vypertester.zeroModulo(6, 0) + assert tx.revert_msg == "Modulo by zero" tx = vypertester.overflow(0, 0, {"value": 31337}) assert tx.revert_msg == "Cannot send ether to nonpayable function" From a4fc7754f1607e70a663803bff5d161dbe9b0af6 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 1 Jan 2020 16:16:40 +0200 Subject: [PATCH 32/39] add Fixed (subclass of Decimal) --- brownie/__init__.py | 3 +- brownie/_cli/console.py | 2 -- brownie/convert.py | 76 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 7 deletions(-) diff --git a/brownie/__init__.py b/brownie/__init__.py index e615f2310..b0d918f1e 100644 --- a/brownie/__init__.py +++ b/brownie/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 from brownie._config import CONFIG as config -from brownie.convert import Wei +from brownie.convert import Fixed, Wei from brownie.project import compile_source, run from brownie.network import accounts, alert, history, rpc, web3 from brownie.network.contract import Contract # NOQA: F401 @@ -16,6 +16,7 @@ "project", "compile_source", "run", + "Fixed", "Wei", "config", ] diff --git a/brownie/_cli/console.py b/brownie/_cli/console.py index 8b06f95b0..cc4d40475 100644 --- a/brownie/_cli/console.py +++ b/brownie/_cli/console.py @@ -4,7 +4,6 @@ import code import importlib import sys -from decimal import Decimal from docopt import docopt @@ -58,7 +57,6 @@ def __init__(self, project=None): try: Gui = importlib.import_module("brownie._gui").Gui locals_dict["Gui"] = Gui - locals_dict["Decimal"] = Decimal except ModuleNotFoundError: pass diff --git a/brownie/convert.py b/brownie/convert.py index b8ce3fc53..c1d1bfad5 100644 --- a/brownie/convert.py +++ b/brownie/convert.py @@ -130,12 +130,80 @@ def _check_int_size(type_: Any) -> int: return size -def to_decimal(value: Any) -> Decimal: +class Fixed(Decimal): + + """ + Decimal subclass that allows comparison against strings, integers and Wei. + + Raises TypeError when operations are attempted against floats. + """ + + # Known typing error: https://github.com/python/mypy/issues/4290 + def __new__(cls, value: Any) -> Any: # type: ignore + return super().__new__(cls, _to_fixed(value)) # type: ignore + + def __repr__(self): + return f"Fixed('{str(self)}')" + + def __hash__(self) -> int: + return super().__hash__() + + def __lt__(self, other: Any) -> bool: # type: ignore + return super().__lt__(_to_fixed(other)) + + def __le__(self, other: Any) -> bool: # type: ignore + return super().__le__(_to_fixed(other)) + + def __eq__(self, other: Any) -> bool: # type: ignore + if isinstance(other, float): + raise TypeError("Cannot compare to floating point - use a string instead") + try: + return super().__eq__(_to_fixed(other)) + except TypeError: + return False + + def __ne__(self, other: Any) -> bool: + if isinstance(other, float): + raise TypeError("Cannot compare to floating point - use a string instead") + try: + return super().__ne__(_to_fixed(other)) + except TypeError: + return True + + def __ge__(self, other: Any) -> bool: # type: ignore + return super().__ge__(_to_fixed(other)) + + def __gt__(self, other: Any) -> bool: # type: ignore + return super().__gt__(_to_fixed(other)) + + def __add__(self, other: Any) -> "Fixed": # type: ignore + return Fixed(super().__add__(_to_fixed(other))) + + def __sub__(self, other: Any) -> "Fixed": # type: ignore + return Fixed(super().__sub__(_to_fixed(other))) + + +def _to_fixed(value: Any) -> Decimal: if isinstance(value, float): - raise TypeError("Cannot cast float to decimal") - if not isinstance(value, (Decimal, int, str)): + raise TypeError("Cannot convert float to decimal - use a string instead") + elif isinstance(value, (str, bytes)): + try: + value = Wei(value) + except TypeError: + pass + + # elif not isinstance(value, (Decimal, int, str)): + # raise TypeError(f"Cannot convert {type(value)} '{value}' to decimal.") + # if isinstance(value, str) and " " in value: + # value = Wei(value) + try: + return Decimal(value) + except Exception: raise TypeError(f"Cannot convert {type(value)} '{value}' to decimal.") - d: Decimal = Decimal(value) + + +def to_decimal(value: Any) -> Fixed: + d: Fixed = Fixed(value) if d < -2 ** 127 or d >= 2 ** 127: raise OverflowError(f"{value} is outside allowable range for decimal") if d.quantize(Decimal("1.0000000000")) != d: From d47d6e170204cde47d6893f9d06433fcb9306d2e Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 1 Jan 2020 16:53:47 +0200 Subject: [PATCH 33/39] add test cases for Fixed and to_decimal --- tests/main/convert/test_fixed.py | 86 +++++++++++++++++++++++ tests/main/convert/test_return_value.py | 9 ++- tests/main/convert/test_to_decimal.py | 43 ++++++++++++ tests/network/contract/test_contracttx.py | 6 ++ 4 files changed, 142 insertions(+), 2 deletions(-) create mode 100755 tests/main/convert/test_fixed.py create mode 100755 tests/main/convert/test_to_decimal.py diff --git a/tests/main/convert/test_fixed.py b/tests/main/convert/test_fixed.py new file mode 100755 index 000000000..3f50a60fd --- /dev/null +++ b/tests/main/convert/test_fixed.py @@ -0,0 +1,86 @@ +#!/usr/bin/python3 + +from decimal import Decimal + +import pytest + +from brownie.convert import Fixed + + +def test_nonetype(): + with pytest.raises(TypeError): + Fixed(None) + + +def test_bytes(): + assert Fixed(b"\xff") == 255 + + +def test_scientific_notation(): + assert Fixed("8.32e26") == 832000000000000000000000000 + + +def test_float(): + with pytest.raises(TypeError): + Fixed(1.99) + + +def test_int(): + assert Fixed(1000) == 1000 + + +def test_str(): + assert Fixed("1000.123456789123456789") == Decimal("1000.123456789123456789") + + +def test_hexstr(): + assert Fixed("0xff") == 255 + + +def test_string_with_unit(): + assert Fixed("3.66 ether") == 3660000000000000000 + assert Fixed("89.006 gwei") == 89006000000 + + +def test_type(): + assert type(Fixed(12)) is Fixed + + +def test_eq(): + assert Fixed("1") == 1 + assert not Fixed("123") == "obviously not a number" + with pytest.raises(TypeError): + Fixed("1.0") == 1.0 + + +def test_ne(): + assert Fixed("1") != 2 + assert Fixed("123") != "obviously not a number" + with pytest.raises(TypeError): + Fixed("1.0") != 1.1 + + +def test_lt(): + assert Fixed("1 ether") < "2 ether" + with pytest.raises(TypeError): + Fixed("1.0") < 1.1 + + +def test_le(): + assert Fixed("1 ether") <= "2 ether" + assert Fixed("1 ether") <= "1 ether" + with pytest.raises(TypeError): + Fixed("1.0") <= 1.1 + + +def test_gt(): + assert Fixed("2 ether") > "1 ether" + with pytest.raises(TypeError): + Fixed("2.0") > 1.0 + + +def test_ge(): + assert Fixed("2 ether") >= "1 ether" + assert Fixed("2 ether") >= "2 ether" + with pytest.raises(TypeError): + Fixed("2.0") >= 2.0 diff --git a/tests/main/convert/test_return_value.py b/tests/main/convert/test_return_value.py index 898bb1d1c..bb807d133 100644 --- a/tests/main/convert/test_return_value.py +++ b/tests/main/convert/test_return_value.py @@ -108,12 +108,17 @@ def test_hexstring_typeerror(): assert str(b) != "potato" -def test_hexstring_length(return_value): +def test_hexstring_length(): b = HexString("0x1234", "bytes32") assert b == "0x1234" assert b == "0x000000000000001234" -def test_hashable(return_value): +def test_hashable(): assert hash(ReturnValue([1, 2])) == hash(tuple([1, 2])) assert set(ReturnValue([3, 1, 3, 3, 7])) == set([3, 1, 3, 3, 7]) + + +def test_decimals(vypertester): + ret = vypertester.fixedType("1.234", ["-42", "3.1337"]) + assert ret == ["1.234", "-42", "3.1337"] diff --git a/tests/main/convert/test_to_decimal.py b/tests/main/convert/test_to_decimal.py new file mode 100755 index 000000000..ffe47e78a --- /dev/null +++ b/tests/main/convert/test_to_decimal.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 + +from decimal import Decimal + +import pytest + +from brownie.convert import Fixed, to_decimal + + +def test_return_type(): + assert isinstance(to_decimal(123), Decimal) + assert type(to_decimal(123)) is Fixed + + +def test_success(): + assert to_decimal(123) == 123 + assert to_decimal("-3.1337") == Decimal("-3.1337") + assert to_decimal("1 ether") == 1000000000000000000 + assert to_decimal(Decimal(42)) == 42 + assert to_decimal(Fixed(6)) == "6" + + +def test_incorrect_type(): + with pytest.raises(TypeError, match="Cannot convert float to decimal - use a string instead"): + to_decimal(3.1337) + with pytest.raises(TypeError): + to_decimal(None) + + +def test_bounds(): + to_decimal(-2 ** 127) + with pytest.raises(OverflowError): + to_decimal(-2 ** 127 - 1) + to_decimal(2 ** 127 - 1) + with pytest.raises(OverflowError): + to_decimal(2 ** 127) + + +def test_decimal_points(): + to_decimal("1.0000000001") + to_decimal("1.00000000010000000") + with pytest.raises(ValueError): + to_decimal("1.00000000001") diff --git a/tests/network/contract/test_contracttx.py b/tests/network/contract/test_contracttx.py index c73567296..1828bec52 100644 --- a/tests/network/contract/test_contracttx.py +++ b/tests/network/contract/test_contracttx.py @@ -164,6 +164,12 @@ def test_repr(tester): repr(tester.revertStrings) +def test_repr_fixedtype(vypertester): + r = repr(vypertester.fixedType) + assert "decimal" in r + assert "fixed168x10" not in r + + def test_tuples(tester, accounts): value = ["blahblah", accounts[1], ["yesyesyes", "0x1234"]] tx = tester.setTuple(value) From f668a9a0cf24129e79463b7b71db1593a06341c0 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 1 Jan 2020 17:02:03 +0200 Subject: [PATCH 34/39] bugfix - repr subsitutions for array types --- brownie/network/contract.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/brownie/network/contract.py b/brownie/network/contract.py index 504e85749..b5be8a2c7 100644 --- a/brownie/network/contract.py +++ b/brownie/network/contract.py @@ -536,7 +536,11 @@ def _params(abi_params: List, substitutions: Optional[Dict] = None) -> List: substitutions = {} for i in abi_params: if i["type"] != "tuple": - types.append((i["name"], substitutions.get(i["type"], i["type"]))) + type_ = i["type"] + for orig, sub in substitutions.items(): + if type_.startswith(orig): + type_ = type_.replace(orig, sub) + types.append((i["name"], type_)) continue params = [i[1] for i in _params(i["components"], substitutions)] types.append((i["name"], f"({','.join(params)})")) From 8d7543e68b1b6474eefb5cfd6a650955e3915e8a Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 1 Jan 2020 18:37:22 +0200 Subject: [PATCH 35/39] update api docs and docstrings, add sphinx to requirements --- brownie/convert.py | 6 +---- docs/api-brownie.rst | 34 +++++++++++++++++++++++++ docs/api-project.rst | 60 +++++--------------------------------------- requirements-dev.txt | 2 ++ 4 files changed, 43 insertions(+), 59 deletions(-) diff --git a/brownie/convert.py b/brownie/convert.py index c1d1bfad5..7491d653d 100644 --- a/brownie/convert.py +++ b/brownie/convert.py @@ -191,11 +191,6 @@ def _to_fixed(value: Any) -> Decimal: value = Wei(value) except TypeError: pass - - # elif not isinstance(value, (Decimal, int, str)): - # raise TypeError(f"Cannot convert {type(value)} '{value}' to decimal.") - # if isinstance(value, str) and " " in value: - # value = Wei(value) try: return Decimal(value) except Exception: @@ -203,6 +198,7 @@ def _to_fixed(value: Any) -> Decimal: def to_decimal(value: Any) -> Fixed: + """Convert a value to a fixed point decimal""" d: Fixed = Fixed(value) if d < -2 ** 127 or d >= 2 ** 127: raise OverflowError(f"{value} is outside allowable range for decimal") diff --git a/docs/api-brownie.rst b/docs/api-brownie.rst index fe14d3826..1cbf00950 100644 --- a/docs/api-brownie.rst +++ b/docs/api-brownie.rst @@ -38,6 +38,10 @@ The following classes and methods are used to convert arguments supplied to ``Co Converts a value to a signed integer. This is equivalent to calling ``Wei`` and then applying checks for over/underflows. +.. py:method:: brownie.convert.to_decimal(value) + + Converts a value to a decimal fixed point and applies bounds according to Vyper's `decimal `_ type. + .. py:method:: brownie.convert.to_bool(value) Converts a value to a boolean. Raises ``ValueError`` if the given value does not match a value in ``(True, False, 0, 1)``. @@ -118,6 +122,32 @@ For certain types of contract data, Brownie uses subclasses to assist with conve >>> Wei("1 ether") - "0.75 ether" 250000000000000000 +.. _fixed: + +.. py:class:: brownie.convert.Fixed(value) + + `Decimal `_ subclass that allows comparisons, addition and subtraction against strings, integers and :ref:`wei`. + + ``Fixed`` is used for inputs and outputs to Vyper contracts that use the `decimal `_ type. + + Attempting comparisons or arithmetic against a float raises a ``TypeError``. + + .. code-block:: python + + >>> from brownie import Fixed + >>> Fixed(1) + Fixed('1') + >>> Fixed(3.1337) + TypeError: Cannot convert float to decimal - use a string instead + >>> Fixed("3.1337") + Fixed('3.1337') + >>> Fixed("12.49 gwei") + Fixed('12490000000') + >>> Fixed("-1.23") == -1.2300 + TypeError: Cannot compare to floating point - use a string instead + >>> Fixed("-1.23") == "-1.2300" + True + .. py:class:: brownie.convert.EthAddress(value) String subclass for address comparisons. Raises a ``TypeError`` when compared to a non-address. @@ -335,6 +365,10 @@ The ``exceptions`` module contains all Brownie ``Exception`` classes. Raised when an ENS name is unset (resolves to ``0x00``). +.. py:exception:: brownie.exceptions.UnsupportedLanguage + + Raised when attempting to compile a language that Brownie does not support. + .. py:exception:: brownie.exceptions.RPCConnectionError Raised when the RPC process is active and ``web3`` is connected, but Brownie is unable to communicate with it. diff --git a/docs/api-project.rst b/docs/api-project.rst index f46078470..ef7cf7bd4 100644 --- a/docs/api-project.rst +++ b/docs/api-project.rst @@ -145,7 +145,9 @@ Module Methods .. py:method:: main.compile_source(source, solc_version=None, optimize=True, runs=200, evm_version=None) - Compiles the given Solidity source code string and returns a ``TempProject`` object. + Compiles the given source code string and returns a ``TempProject`` object. + + If Vyper source code is given, the contract name will be ``Vyper``. .. code-block:: python @@ -353,7 +355,7 @@ Module Methods Returns a ``dict`` of ``{'version': ["path", "path", ..]}``. -.. py:method:: compiler.generate_input_json(contract_sources, optimize=True, runs=200, evm_version=None, minify=False) +.. py:method:: compiler.generate_input_json(contract_sources, optimize=True, runs=200, evm_version=None, minify=False, language="Solidity") Generates a `standard solc input JSON `_ as a dict. @@ -370,56 +372,6 @@ Module Methods * ``compiler_data``: Additional compiler data to include * ``silent``: Toggles console verbosity -Internal Methods ----------------- - -.. py:method:: compiler._format_link_references(evm) - - Standardizes formatting for unlinked library placeholders within bytecode. Used internally to ensure that unlinked libraries are represented uniformly regardless of the compiler version used. - - * ``evm``: The ``'evm'`` object from a compiler output JSON. - -.. py:method:: compiler._get_bytecode_hash(bytecode) - - Removes the final metadata from a bytecode hex string and returns a hash of the result. Used to check if a contract has changed when the source code is modified. - -.. py:method:: compiler._expand_source_map(source_map) - - Returns an uncompressed source mapping as a list of lists where no values are omitted. - - .. code-block:: python - - >>> from brownie.project.compiler import expand_source_map - >>> expand_source_map("1:2:1:-;:9;2:1:2;;;") - [[1, 2, 1, '-'], [1, 9, 1, '-'], [2, 1, 2, '-'], [2, 1, 2, '-'], [2, 1, 2, '-'], [2, 1, 2, '-']] - -.. py:method:: compiler._generate_coverage_data(source_map_str, opcodes_str, contract_node, stmt_nodes, branch_nodes, has_fallback) - - Generates the `program counter `_ and `coverage `_ maps that are used by Brownie for debugging and test coverage evaluation. - - Takes the following arguments: - - * ``source_map_str``: `deployed source mapping `_ as given by the compiler - * ``opcodes_str``: deployed bytecode opcodes string as given by the compiler - * ``contract_node``: py-solc-ast contract node object - * ``stmt_nodes``: list of statement node objects from ``compiler.get_statment_nodes`` - * ``branch_nodes``: list of branch node objects from ``compiler.get_branch_nodes`` - * ``has_fallback``: Bool, does this contract contain a fallback method? - - Returns: - - * ``pc_list``: program counter map - * ``statement_map``: statement coverage map - * ``branch_map``: branch coverage map - -.. py:method:: compiler._get_statement_nodes(source_nodes) - - Given a list of AST source node objects from `py-solc-ast `_, returns a list of statement nodes. Used to generate the statement coverage map. - -.. py:method:: compiler._get_branch_nodes(source_nodes) - - Given a list of AST source node objects from `py-solc-ast `_, returns a list of branch nodes. Used to generate the branch coverage map. - ``brownie.project.ethpm`` ========================= @@ -624,7 +576,7 @@ Module Methods .. _sources-minify: -.. py:method:: sources.minify(source) +.. py:method:: sources.minify(source, language="Solidity") Given contract source as a string, returns a minified version and an offset map used internally to translate minified offsets to the original ones. @@ -650,6 +602,6 @@ Module Methods Given a path, start and stop offset, returns highlighted source code. Called internally by ``TransactionReceipt.source``. -.. py:method:: sources.get_hash(source, contract_name, minified) +.. py:method:: sources.get_hash(source, contract_name, minified, language) Returns a sha1 hash generated from a contract's source code. diff --git a/requirements-dev.txt b/requirements-dev.txt index 0150b098e..414459c6d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,6 +10,8 @@ pytest-cov==2.8.1 pytest-mock==1.12.0 pytest-xdist==1.31.0 sphinx==2.0.1 +sphinx_rtd_theme==0.4.3 +pygments_lexer_solidity==0.4.0 tox==3.14.0 twine==1.13.0 wheel==0.33.4 From 0b717f6b2d4611b128b2448019d8501b1cd1c50c Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Wed, 1 Jan 2020 19:53:08 +0200 Subject: [PATCH 36/39] more docs updates --- README.md | 10 +++++++++- docs/compile.rst | 7 ++++++- docs/gui.rst | 4 ++++ docs/index.rst | 15 ++++++++------- docs/init.rst | 2 +- docs/install.rst | 22 +++++++++++++++++++++- docs/quickstart.rst | 2 +- docs/tests.rst | 2 +- 8 files changed, 51 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e91c04ff3..89f464478 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,15 @@ [![Pypi Status](https://img.shields.io/pypi/v/eth-brownie.svg)](https://pypi.org/project/eth-brownie/) [![Build Status](https://travis-ci.com/iamdefinitelyahuman/brownie.svg?branch=master)](https://travis-ci.com/iamdefinitelyahuman/brownie) [![Docs Status](https://readthedocs.org/projects/eth-brownie/badge/?version=latest)](https://eth-brownie.readthedocs.io/en/latest/) [![Coverage Status](https://coveralls.io/repos/github/iamdefinitelyahuman/brownie/badge.svg?branch=master)](https://coveralls.io/github/iamdefinitelyahuman/brownie?branch=master) -Brownie is a Python framework for deploying, testing and interacting with Ethereum smart contracts. +Brownie is a Python-based development and testing framework for smart contracts targeting the [Ethereum Virtual Machine](https://solidity.readthedocs.io/en/v0.6.0/introduction-to-smart-contracts.html#the-ethereum-virtual-machine). + +## Features + +* Full support for [Solidity](https://github.com/ethereum/solidity) (`>=0.4.22`) and [Vyper](https://github.com/vyperlang/vyper) (`0.1.0b-15`) +* Contract testing via [pytest](https://github.com/pytest-dev/pytest), including trace-based coverage evaluation +* Powerful debugging tools, including python-style tracebacks and custom error strings +* Built-in console for quick project interaction +* Support for [ethPM](https://www.ethpm.com) packages ## Dependencies diff --git a/docs/compile.rst b/docs/compile.rst index 0e1e7eb90..0953cbb36 100644 --- a/docs/compile.rst +++ b/docs/compile.rst @@ -12,6 +12,11 @@ To compile a project: Each time the compiler runs, Brownie compares hashes of the contract source code against the existing compiled versions. If a contract has not changed it will not be recompiled. If you wish to force a recompile of the entire project, use ``brownie compile --all``. +Brownie supports both Solidity and Vyper. Which compiler to use is determined based on the suffix of the file: + + * Solidity: ``.sol`` + * Vyper: ``.vy`` + .. note:: All of a project's contract sources must be placed inside the ``contracts/`` folder. Attempting to import sources from outside this folder will result in a compiler error. @@ -39,7 +44,7 @@ Setting the Compiler Version .. note:: - Brownie supports Solidity versions ``>=0.4.22``. + Brownie supports Solidity versions ``>=0.4.22`` and Vyper version ``0.1.0-b15``. If a compiler version is set in the configuration file, all contracts in the project are compiled using that version. It is installed automatically if not already present. The version should be given as a string in the format ``0.x.x``. diff --git a/docs/gui.rst b/docs/gui.rst index ecebba422..ed90df666 100644 --- a/docs/gui.rst +++ b/docs/gui.rst @@ -23,6 +23,10 @@ Brownie includes a GUI for viewing test coverage data and analyzing the compiled Parts of this section assume a level of familiarity with EVM bytecode. If you are looking to learn more about the subject, Alejandro Santander from `OpenZeppelin `_ has written an excellent guide - `Deconstructing a Solidity Contract `_. +.. note:: + + If you receive an error when attempting to load the GUI, you probably do not have Tk installed on your system. See the :ref:`Tk installation instrucions` for more detailed information. + Getting Started =============== diff --git a/docs/index.rst b/docs/index.rst index 7d19aaaf9..a26bfd1e9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,7 @@ Brownie ======= -Brownie is a Python framework for Ethereum smart contract testing, interaction and deployment. +Brownie is a Python-based development and testing framework for smart contracts targeting the `Ethereum Virtual Machine `_. .. note:: @@ -12,10 +12,11 @@ Brownie is a Python framework for Ethereum smart contract testing, interaction a This project relies heavily upon ``web3.py`` and the documentation assumes a basic familiarity with it. You may wish to view the `Web3.py docs `__ if you have not used it previously. -Brownie has several uses: - -* **Testing**: Unit test your project with ``pytest``, and evaluate test coverage through stack trace analysis. We make *no promises*. -* **Debugging**: Get detailed information when a transaction reverts, to help you locate and solve the issue quickly. -* **Interaction**: Write scripts or use the console to interact with your contracts on the main-net, or for quick testing in a local environment. -* **Deployment**: Automate the deployment of many contracts onto the blockchain, and any transactions needed to initialize or integrate the contracts. +Features +======== +* Full support for `Solidity `_ and `Vyper `_ +* Contract testing via `pytest `_, including trace-based coverage evaluation +* Powerful debugging tools, including python-style tracebacks and custom error strings +* Built-in console for quick project interaction +* Support for `ethPM `_ packages diff --git a/docs/init.rst b/docs/init.rst index e2b8f458e..89d3b2253 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -12,7 +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` +* ``reports/``: JSON report files * ``scripts/``: Scripts for deployment and interaction * ``tests/``: Scripts for testing your project * ``brownie-config.yaml``: Configuration file for the project diff --git a/docs/install.rst b/docs/install.rst index 470b05800..9cfb57686 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -32,6 +32,26 @@ Brownie has the following dependencies: * `ganache-cli `__ * `pip `__ -* `python3 `__ version 3.6 or greater, python3-dev, python3-tk +* `python3 `__ version 3.6 or greater, python3-dev As brownie relies on `py-solc-x `__, you do not need solc installed locally but you must install all required `solc dependencies `__. + + +.. _install-tk: + +Tkinter +------- + +:ref:`The Brownie GUI` is built using the `Tk GUI toolkit `_. Both Tk and `tkinter `_ are available on most Unix platforms, as well as on Windows systems. + +Tk is not a strict dependency for Brownie. However, if it is not installed on your system you will receive an error when attempting to load the GUI. + +You can use the following command to check that Tk has been correctly installed: + +:: + + $ python -m tkinter + +This should open a simple window and display the installed version number. + +For installation instructions read `Installing TK `_ within the TK Documentation. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index a138d3b82..5895ee0c7 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -30,7 +30,7 @@ Before installing Brownie, make sure you have the following dependencies: * `ganache-cli `__ * `pip `__ -* `python3 `__ version 3.6 or greater, python3-dev, python3-tk +* `python3 `__ version 3.6 or greater, python3-dev As brownie relies on `py-solc-x `__, you do not need solc installed locally but you must install all required `solc dependencies `__. diff --git a/docs/tests.rst b/docs/tests.rst index ef958ba1c..860a9f52c 100644 --- a/docs/tests.rst +++ b/docs/tests.rst @@ -158,7 +158,7 @@ Each revert string adds a minimum 20000 gas to your contract deployment cost, an For this reason, Brownie allows you to include revert strings as source code comments that are not included in the bytecode but still accessible via ``TransactionReceipt.revert_msg``. You write tests that target a specific ``require`` or ``revert`` statement without increasing gas costs. -Revert string comments must begin with ``// dev:`` in order for Brownie to recognize them. Priority is always given to compiled revert strings. Some examples: +Revert string comments must begin with ``// dev:`` in Solidity, or ``# dev:`` in Vyper. Priority is always given to compiled revert strings. Some examples: .. code-block:: solidity :linenos: From bb2580b87293a19128719ef57cee2182ec24c128 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Fri, 3 Jan 2020 00:12:23 +0200 Subject: [PATCH 37/39] do not adjust outputSelection for vyper compilation --- brownie/project/compiler/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/brownie/project/compiler/__init__.py b/brownie/project/compiler/__init__.py index 992ad0c91..7e633f8b8 100644 --- a/brownie/project/compiler/__init__.py +++ b/brownie/project/compiler/__init__.py @@ -135,8 +135,6 @@ def generate_input_json( input_json["settings"]["evmVersion"] = evm_version if language == "Solidity": input_json["settings"]["optimizer"] = {"enabled": optimize, "runs": runs if optimize else 0} - else: - input_json["outputSelection"] = input_json["settings"].pop("outputSelection") input_json["sources"] = dict( (k, {"content": sources.minify(v, language)[0] if minify else v}) for k, v in contract_sources.items() From 60ed067bb67f758dea3f2ebdb65513865e69d9b9 Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 7 Jan 2020 21:35:47 +0100 Subject: [PATCH 38/39] modify travis builds, update changelog, bump version --- .travis.yml | 46 ++++++++++++++++++++-------------------- CHANGELOG.md | 3 +++ brownie/_cli/__main__.py | 2 +- docs/conf.py | 2 +- requirements.txt | 2 +- setup.cfg | 2 +- setup.py | 2 +- 7 files changed, 31 insertions(+), 28 deletions(-) diff --git a/.travis.yml b/.travis.yml index 92f02c614..bfef7d6a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ # Based on https://github.com/cclauss/Travis-CI-Python-on-three-OSes matrix: include: - - name: "Standard Tests, Parametrized EVM Tests - Python 3.7.1 on Xenial Linux" + - name: "EVM Tests - Python 3.7 on Xenial Linux" language: python python: 3.7 dist: xenial @@ -13,21 +13,8 @@ matrix: - sudo apt-get install -y python3.7-dev npm solc - npm -g install ganache-cli@6.7.0 - python -m pip install coveralls==1.9.2 tox==3.14.0 - script: tox -e py37,evmtests - - name: "Standard Tests, Linting, Docs - Python 3.6.8 on Xenial Linux" - language: python - python: 3.6 - dist: xenial - sudo: true - install: - - sudo add-apt-repository -y ppa:ethereum/ethereum - - sudo add-apt-repository -y ppa:deadsnakes/ppa - - sudo apt-get update - - sudo apt-get install -y python3.6-dev npm solc - - npm -g install ganache-cli@6.7.0 - - python -m pip install coveralls==1.9.2 tox==3.14.0 - script: tox -e lint,doctest,py36 - - name: "Standard Tests - Python 3.7.4 on Windows" + script: tox -e evmtests + - name: "Standard Tests - Python 3.7 on Windows" os: windows language: node_js node_js: '10' @@ -39,22 +26,22 @@ matrix: - python setup.py install env: PATH=/c/Python37:/c/Python37/Scripts:$PATH script: python -m pytest tests/ --cov=brownie/ -p no:pytest-brownie -n auto --dist=loadscope - - name: "Standard Tests - Python 3.8.0 on Xenial Linux" + - name: "Standard Tests, Linting, Docs - Python 3.6 on Xenial Linux" language: python - python: 3.8 + python: 3.6 dist: xenial sudo: true install: - sudo add-apt-repository -y ppa:ethereum/ethereum - sudo add-apt-repository -y ppa:deadsnakes/ppa - sudo apt-get update - - sudo apt-get install -y python3.8-dev npm solc + - sudo apt-get install -y python3.6-dev npm solc - npm -g install ganache-cli@6.7.0 - python -m pip install coveralls==1.9.2 tox==3.14.0 - script: tox -e py38 - - name: "Brownie Mix Tests - Python 3.6.8 on Xenial Linux" + script: tox -e lint,doctest,py36 + - name: "Standard Tests, Brownie Mix Tests - Python 3.7 on Xenial Linux" language: python - python: 3.6 + python: 3.7 dist: xenial sudo: true install: @@ -64,7 +51,20 @@ matrix: - sudo apt-get install -y python3.6-dev npm solc - npm -g install ganache-cli@6.7.0 - python -m pip install coveralls==1.9.2 tox==3.14.0 - script: tox -e mixtests + script: tox -e py37,mixtests + - name: "Standard Tests - Python 3.8 on Xenial Linux" + language: python + python: 3.8 + dist: xenial + sudo: true + install: + - sudo add-apt-repository -y ppa:ethereum/ethereum + - sudo add-apt-repository -y ppa:deadsnakes/ppa + - sudo apt-get update + - sudo apt-get install -y python3.8-dev npm solc + - npm -g install ganache-cli@6.7.0 + - python -m pip install coveralls==1.9.2 tox==3.14.0 + script: tox -e py38 env: global: COVERALLS_PARALLEL=true diff --git a/CHANGELOG.md b/CHANGELOG.md index db2f01385..a6181ae43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,10 @@ This changelog format is based on [Keep a Changelog](https://keepachangelog.com/ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased](https://github.com/iamdefinitelyahuman/brownie) + +## [1.4.0](https://github.com/iamdefinitelyahuman/brownie/tree/v1.4.0) - 2020-01-07 ### Added +- support for Vyper smart contracts ([v0.1.0-beta15](https://github.com/vyperlang/vyper/releases/tag/v0.1.0-beta.15)) - `brownie accounts` commandline interface ## [1.3.2](https://github.com/iamdefinitelyahuman/brownie/tree/v1.3.2) - 2020-01-01 diff --git a/brownie/_cli/__main__.py b/brownie/_cli/__main__.py index e410d8151..1201c6cd0 100644 --- a/brownie/_cli/__main__.py +++ b/brownie/_cli/__main__.py @@ -11,7 +11,7 @@ from brownie.exceptions import ProjectNotFound from brownie.utils import color, notify -__version__ = "1.3.2" +__version__ = "1.4.0" __doc__ = """Usage: brownie [...] [options ] diff --git a/docs/conf.py b/docs/conf.py index e08459471..a37f89d48 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -37,7 +37,7 @@ def setup(sphinx): # The short X.Y version version = "" # The full version, including alpha/beta/rc tags -release = "v1.3.2" +release = "v1.4.0" # -- General configuration --------------------------------------------------- diff --git a/requirements.txt b/requirements.txt index a93f6cee3..c9faaade3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,5 +15,5 @@ pyyaml>=5.1.0,<6.0.0 requests>=2.22.0,<3.0.0 semantic-version>=2.8.2,<3.0.0 tqdm==4.41.0 -vyper==0.1.0b14 +vyper==0.1.0b15 web3==5.3.0 diff --git a/setup.cfg b/setup.cfg index 30c052f09..ebe5a06db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.3.2 +current_version = 1.4.0 [bumpversion:file:setup.py] diff --git a/setup.py b/setup.py index 67b27aed7..66631f836 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ setup( name="eth-brownie", packages=find_packages(), - version="1.3.2", # don't change this manually, use bumpversion instead + version="1.4.0", # don't change this manually, use bumpversion instead license="MIT", description="A Python framework for Ethereum smart contract deployment, testing and interaction.", # noqa: E501 long_description=long_description, From 604c15a330f4b47bb0a0f4aae1528cc34126567d Mon Sep 17 00:00:00 2001 From: iamdefinitelyahuman Date: Tue, 7 Jan 2020 22:24:47 +0100 Subject: [PATCH 39/39] fix flaky/failing tests, update gitignore to include xdist coverage files --- .gitignore | 2 +- tests/conftest.py | 2 +- tests/network/test_alert.py | 4 ++-- tests/project/compiler/test_solidity.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 827f5025a..06ec5c202 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ __pycache__ .vscode .history -.coverage +.coverage* build/ brownie/data/*/ brownie/data/*.json diff --git a/tests/conftest.py b/tests/conftest.py index beeffb4ef..b6348d1c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ def pytest_sessionstart(): monkeypatch_session = MonkeyPatch() monkeypatch_session.setattr( "solcx.get_available_solc_versions", - lambda: ["v0.5.10", "v0.5.9", "v0.5.8", "v0.5.7", "v0.4.25", "v0.4.24", "v0.4.22"], + lambda: ["v0.6.0", "v0.5.10", "v0.5.8", "v0.5.7", "v0.4.25", "v0.4.24", "v0.4.22"], ) diff --git a/tests/network/test_alert.py b/tests/network/test_alert.py index 1123fb3ab..70fb0215d 100644 --- a/tests/network/test_alert.py +++ b/tests/network/test_alert.py @@ -84,10 +84,10 @@ def test_stop(): def test_fire_msg(capfd): t = AlertTest(False) - alert.new(t, delay=0.02, msg="Fired") + alert.new(t, delay=0.03, msg="Fired") assert not capfd.readouterr()[0].strip() t.set_value(True) - time.sleep(0.04) + time.sleep(0.08) assert capfd.readouterr()[0].strip()[-5:] == "Fired" assert len(alert.show()) == 0 diff --git a/tests/project/compiler/test_solidity.py b/tests/project/compiler/test_solidity.py index 40d177f1b..a7496fa54 100644 --- a/tests/project/compiler/test_solidity.py +++ b/tests/project/compiler/test_solidity.py @@ -175,9 +175,9 @@ def test_find_solc_versions_install(find_version, msolc): find_version("^0.4.22", install_latest=True) assert msolc.pop() == "v0.4.25" find_version("^0.4.24 || >=0.5.10", install_needed=True) - assert msolc.pop() == "v0.5.10" + assert msolc.pop() == "v0.6.0" find_version(">=0.4.24", install_latest=True) - assert msolc.pop() == "v0.5.10" + assert msolc.pop() == "v0.6.0" def test_install_solc(msolc):