Skip to content

Commit

Permalink
Merge pull request #363 from iamdefinitelyahuman/travis-updates
Browse files Browse the repository at this point in the history
a day I'll never get back...
  • Loading branch information
iamdefinitelyahuman committed Feb 19, 2020
2 parents 9af93db + ee7d3c6 commit ddb104c
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 74 deletions.
16 changes: 8 additions & 8 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Based on https://github.com/cclauss/Travis-CI-Python-on-three-OSes
jobs:
include:
- name: "EVM Tests - Python 3.7 on Xenial Linux"
- name: "EVM Tests - Python 3.7 on Bionic Linux"
language: python
python: 3.7
dist: xenial
dist: bionic
before_install:
- sudo add-apt-repository -y ppa:ethereum/ethereum
- sudo add-apt-repository -y ppa:deadsnakes/ppa
Expand All @@ -19,30 +19,30 @@ jobs:
- choco install python --version=3.8.0
env: PATH=/c/Python38:/c/Python38/Scripts:$PATH
script: tox -e py38
- name: "Standard Tests, Linting, Docs - Python 3.6 on Xenial Linux"
- name: "Standard Tests, Linting, Docs - Python 3.6 on Bionic Linux"
language: python
python: 3.6
dist: xenial
dist: bionic
before_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
script: tox -e lint,doctest,py36
- name: "Standard Tests, Brownie Mix Tests - Python 3.7 on Xenial Linux"
- name: "Standard Tests, Brownie Mix Tests - Python 3.7 on Bionic Linux"
language: python
python: 3.7
dist: xenial
dist: bionic
before_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
script: tox -e py37,mixtest
- name: "Standard Tests, Plugin Tests - Python 3.8 on Xenial Linux"
- name: "Standard Tests, Plugin Tests - Python 3.8 on Bionic Linux"
language: python
python: 3.8
dist: xenial
dist: bionic
before_install:
- sudo add-apt-repository -y ppa:ethereum/ethereum
- sudo add-apt-repository -y ppa:deadsnakes/ppa
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/iamdefinitelyahuman/brownie)

## [1.6.5](https://github.com/iamdefinitelyahuman/brownie/tree/v1.6.5) - 2020-02-19

### Fixed
- Fix issues from missing source offsets in Solidity [v0.6.3](https://github.com/ethereum/solidity/releases/tag/v0.6.3)
- Do not assume pytest will run test functions sequentially (adds support for `-k` flag)

## [1.6.4](https://github.com/iamdefinitelyahuman/brownie/tree/v1.6.4) - 2020-02-11
### Added
- Show progress spinner when running stateful tests
Expand Down
2 changes: 1 addition & 1 deletion brownie/_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from brownie.utils import color, notify
from brownie.utils.docopt import docopt, levenshtein_norm

__version__ = "1.6.4"
__version__ = "1.6.5"

__doc__ = """Usage: brownie <command> [<args>...] [options <args>]
Expand Down
165 changes: 111 additions & 54 deletions brownie/project/compiler/solidity.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import solcx
from requests.exceptions import ConnectionError
from semantic_version import Version
from solcast.nodes import NodeBase
from solcast.nodes import NodeBase, is_inside_offset

from brownie._config import EVM_EQUIVALENTS
from brownie.exceptions import CompilerError, IncompatibleSolcVersion
Expand Down Expand Up @@ -52,13 +52,11 @@ def compile_from_input_json(
input_json["settings"]["evmVersion"] = EVM_EQUIVALENTS[input_json["settings"]["evmVersion"]]

if not silent:
print("Compiling contracts...")
print(f" Solc version: {str(solcx.get_solc_version())}")
print("Compiling contracts...\n Solc version: {str(solcx.get_solc_version())}")

opt = "Enabled Runs: {optimizer['runs']}" if optimizer["enabled"] else "Disabled"
print(f" Optimizer: {opt}")

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()}")

Expand Down Expand Up @@ -310,8 +308,12 @@ def _generate_coverage_data(
revert_map: Dict = {}
fallback_hexstr: str = "unassigned"

active_source_node: Optional[NodeBase] = None
active_fn_node: Optional[NodeBase] = None
active_fn_name: Optional[str] = None

while source_map:
# format of source is [start, stop, contract_id, jump code]
# format of source_map is [start, stop, contract_id, jump code]
source = source_map.popleft()
pc_list.append({"op": opcodes.popleft(), "pc": pc})

Expand All @@ -323,7 +325,7 @@ def _generate_coverage_data(
):
# flag the REVERT op at the end of the function selector,
# later reverts may jump to it instead of having their own REVERT op
fallback_hexstr = "0x" + hex(pc - 4).upper()[2:]
fallback_hexstr = f"0x{hex(pc - 4).upper()[2:]}"
pc_list[-1]["first_revert"] = True

if source[3] != "-":
Expand All @@ -334,19 +336,17 @@ def _generate_coverage_data(
pc_list[-1]["value"] = opcodes.popleft()
pc += int(pc_list[-1]["op"][4:])

# set contract path (-1 means none)
# for REVERT opcodes without an source offset, try to infer one
if source[2] == -1:
if pc_list[-1]["op"] == "REVERT" and pc_list[-8]["op"] == "CALLVALUE":
pc_list[-1].update(
{
"dev": "Cannot send ether to nonpayable function",
"fn": pc_list[-8].get("fn", "<unknown>"),
"offset": pc_list[-8]["offset"],
"path": pc_list[-8]["path"],
}
if pc_list[-1]["op"] == "REVERT":
_find_revert_offset(
pc_list, source_map, active_source_node, active_fn_node, active_fn_name
)
continue
path = source_nodes[source[2]].absolutePath

# set contract path (-1 means none)
active_source_node = source_nodes[source[2]]
path = active_source_node.absolutePath
pc_list[-1]["path"] = path

# set source offset (-1 means none)
Expand All @@ -357,16 +357,9 @@ def _generate_coverage_data(

# add error messages for INVALID opcodes
if pc_list[-1]["op"] == "INVALID":
node = source_nodes[source[2]].children(include_children=False, offset_limits=offset)[0]
if node.nodeType == "IndexAccess":
pc_list[-1]["dev"] = "Index out of range"
elif node.nodeType == "BinaryOperation":
if node.operator == "/":
pc_list[-1]["dev"] = "Division by zero"
elif node.operator == "%":
pc_list[-1]["dev"] = "Modulus by zero"

# if op is jumpi, set active branch markers
_set_invalid_error_string(active_source_node, pc_list[-1])

# for JUMPI instructions, set active branch markers
if branch_active[path] and pc_list[-1]["op"] == "JUMPI":
for offset in branch_active[path]:
# ( program counter index, JUMPI index)
Expand All @@ -382,25 +375,24 @@ def _generate_coverage_data(
try:
# set fn name and statement coverage marker
if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]:
pc_list[-1]["fn"] = pc_list[-2]["fn"]
pc_list[-1]["fn"] = active_fn_name
else:
pc_list[-1]["fn"] = _get_fn_full_name(source_nodes[source[2]], offset)
active_fn_node, active_fn_name = _get_active_fn(active_source_node, offset)
pc_list[-1]["fn"] = active_fn_name
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
statement_map[path].setdefault(active_fn_name, {})[count] = stmt_offset
pc_list[-1]["statement"] = count
count += 1
except (KeyError, IndexError, StopIteration):
pass
if "value" not in pc_list[-1]:
continue
if pc_list[-1]["value"] == fallback_hexstr and opcodes[0] in {"JUMP", "JUMPI"}:

if pc_list[-1].get("value", None) == fallback_hexstr and opcodes[0] in ("JUMP", "JUMPI"):
# track all jumps to the initial revert
revert_map.setdefault((pc_list[-1]["path"], pc_list[-1]["offset"]), []).append(
len(pc_list)
)
key = (pc_list[-1]["path"], pc_list[-1]["offset"])
revert_map.setdefault(key, []).append(len(pc_list))

# compare revert() statements against the map of revert jumps to find
for (path, fn_offset), values in revert_map.items():
Expand All @@ -418,40 +410,105 @@ def _generate_coverage_data(
offset = node.offset
# if the node offset is not in the source map, apply it's offset to the JUMPI op
if not next((x for x in pc_list if "offset" in x and x["offset"] == offset), False):
pc_list[values[0]].update({"offset": offset, "jump_revert": True})
pc_list[values[0]].update(offset=offset, jump_revert=True)
del values[0]

# set branch index markers and build final branch map
branch_map: Dict = dict((i, {}) for i in paths)
for path, offset, idx in [(k, x, y) for k, v in branch_set.items() for x, y in v.items()]:
# for branch to be hit, need an op relating to the source and the next JUMPI
# this is because of how the compiler optimizes nested BinaryOperations
if "fn" not in pc_list[idx[0]]:
continue
fn = pc_list[idx[0]]["fn"]
pc_list[idx[0]]["branch"] = count
pc_list[idx[1]]["branch"] = count
node = next(i for i in branch_original[path] if i.offset == offset)
branch_map[path].setdefault(fn, {})[count] = offset + (node.jump,)
count += 1
if "fn" in pc_list[idx[0]]:
fn = pc_list[idx[0]]["fn"]
pc_list[idx[0]]["branch"] = count
pc_list[idx[1]]["branch"] = count
node = next(i for i in branch_original[path] if i.offset == offset)
branch_map[path].setdefault(fn, {})[count] = offset + (node.jump,)
count += 1

pc_map = dict((i.pop("pc"), i) for i in pc_list)
return pc_map, statement_map, branch_map


def _get_fn_full_name(source_node: NodeBase, offset: Tuple[int, int]) -> str:
node = source_node.children(
def _find_revert_offset(
pc_list: List,
source_map: deque,
source_node: NodeBase,
fn_node: NodeBase,
fn_name: Optional[str],
) -> None:

# attempt to infer a source offset for reverts that do not have one

if source_map and source_map[0][2] == -1:
# this is not the last instruction and the following instruction also has no source
if len(pc_list) >= 8 and pc_list[-8]["op"] == "CALLVALUE":
# reference to CALLVALUE 8 instructions previous is a nonpayable function check
pc_list[-1].update(
dev="Cannot send ether to nonpayable function",
fn=pc_list[-8].get("fn", "<unknown>"),
offset=pc_list[-8]["offset"],
path=pc_list[-8]["path"],
)
return

# if there is active function, we are still in the function selector table
if not fn_node:
return

# get the offset of the next instruction
next_offset = None
if source_map and source_map[0][2] != -1:

next_offset = (source_map[0][0], source_map[0][0] + source_map[0][1])

# if the next instruction offset is not equal to the offset of the active function,
# but IS contained within the active function, apply this offset to the current
# instruction

if (
next_offset
and next_offset != fn_node.offset
and is_inside_offset(next_offset, fn_node.offset)
):
pc_list[-1].update(path=source_node.absolutePath, fn=fn_name, offset=next_offset)
return

# if any of the previous conditions are not satisfied, this is the final revert
# statement within a function
if fn_node[-1].nodeType == "ExpressionStatement":
expr = fn_node[-1].expression
if expr.nodeType == "FunctionCall" and expr.expression.name == "revert":
pc_list[-1].update(
path=source_node.absolutePath, fn=fn_name, offset=expr.expression.offset
)


def _set_invalid_error_string(source_node: NodeBase, pc_map: Dict) -> None:
# set custom error string for INVALID opcodes
node = source_node.children(include_children=False, offset_limits=pc_map["offset"])[0]
if node.nodeType == "IndexAccess":
pc_map["dev"] = "Index out of range"
elif node.nodeType == "BinaryOperation":
if node.operator == "/":
pc_map["dev"] = "Division by zero"
elif node.operator == "%":
pc_map["dev"] = "Modulus by zero"


def _get_active_fn(source_node: NodeBase, offset: Tuple[int, int]) -> Tuple[NodeBase, str]:
fn_node = source_node.children(
depth=2, required_offset=offset, filters={"nodeType": "FunctionDefinition"}
)[0]
name = getattr(node, "name", None)
name = getattr(fn_node, "name", None)
if not name:
if getattr(node, "kind", "function") != "function":
name = f"<{node.kind}>"
elif getattr(node, "isConstructor", False):
if getattr(fn_node, "kind", "function") != "function":
name = f"<{fn_node.kind}>"
elif getattr(fn_node, "isConstructor", False):
name = "<constructor>"
else:
name = "<fallback>"
return f"{node.parent().name}.{name}"
return fn_node, f"{fn_node.parent().name}.{name}"


def _get_nodes(output_json: Dict) -> Tuple[Dict, Dict, Dict]:
Expand Down
5 changes: 2 additions & 3 deletions brownie/test/managers/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,8 @@ def pytest_runtest_protocol(self, item):
if path in self.tests and ARGV["update"]:
self.results[path] = list(self.tests[path]["results"])
else:
self.results[path] = []
# all tests are initially marked as skipped
self.results[path] = ["s"] * len(self.node_map[path])

def pytest_runtest_logreport(self, report):
path, test_id = self._test_id(report.nodeid)
Expand All @@ -149,8 +150,6 @@ def pytest_runtest_logreport(self, report):
idx = self.node_map[path].index(test_id)

results = self.results[path]
if report.when == "setup" and len(results) < idx + 1:
results.append("s" if report.skipped else None)
if report.when == "call":
results[idx] = convert_outcome(report.outcome)
if hasattr(report, "wasxfail"):
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def setup(sphinx):
# The short X.Y version
version = ""
# The full version, including alpha/beta/rc tags
release = "v1.6.4"
release = "v1.6.5"


# -- General configuration ---------------------------------------------------
Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ eth-event>=0.2.2,<1.0.0
eth-hash[pycryptodome]==0.2.0
eth-utils==1.8.4
hexbytes==0.2.0
hypothesis==5.5.1
psutil>=5.6.7,<6.0.0
hypothesis==5.5.4
psutil>=5.7.0,<6.0.0
py>=1.5.0
pyreadline==2.1;platform_system=='Windows'
py-solc-ast>=1.1.0,<2.0.0
py-solc-x>=0.7.2,<1.0.0
py-solc-x>=0.8.0,<1.0.0
pytest==5.3.5
pytest-xdist==1.31.0
pythx==1.5.3
pyyaml>=5.3.0,<6.0.0
requests>=2.22.0,<3.0.0
semantic-version>=2.8.4,<3.0.0
tqdm==4.42.1
tqdm==4.43.0
vyper==0.1.0b16
web3==5.5.1
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.6.4
current_version = 1.6.5

[bumpversion:file:setup.py]

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
setup(
name="eth-brownie",
packages=find_packages(),
version="1.6.4", # don't change this manually, use bumpversion instead
version="1.6.5", # 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,
Expand Down
Loading

0 comments on commit ddb104c

Please sign in to comment.