Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: if only single compiler error, avoid wrapping in a new error #2251

Merged
merged 3 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/ape/managers/compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,18 @@ def compile(
errors.append(err)
continue

if errors:
if len(errors) == 1:
# If only 1 error, just raise that.
raise errors[0]

elif len(errors) > 1:
# Raise a combined error.
formatted_errors = [f"{e}" for e in errors]
error_message = "\n\n".join(formatted_errors)
raise CompilerError(error_message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if python 3.11+ exception groups make sense to use when it becomes available to us

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow this is cool!

Idea: could try to work this in for the Python versions that support it and have it the dumb way now


# else: successfully compiled everything!

def compile_source(
self,
compiler_name: str,
Expand Down
86 changes: 47 additions & 39 deletions tests/functional/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,45 +634,53 @@ def method_abi_with_struct_input():


@pytest.fixture
def mock_compiler(mocker):
mock = mocker.MagicMock()
mock.name = "mock"
mock.ext = ".__mock__"
mock.tracked_settings = []
mock.ast = None
mock.pcmap = None

def mock_compile(paths, project=None, settings=None):
settings = settings or {}
mock.tracked_settings.append(settings)
result = []
for path in paths:
if path.suffix == mock.ext:
name = path.stem
code = to_hex(123)
data = {
"contractName": name,
"abi": mock.abi,
"deploymentBytecode": code,
"sourceId": f"{project.contracts_folder.name}/{path.name}",
}
if ast := mock.ast:
data["ast"] = ast
if pcmap := mock.pcmap:
data["pcmap"] = pcmap

# Check for mocked overrides
overrides = mock.overrides
if isinstance(overrides, dict):
data = {**data, **overrides}

contract_type = ContractType.model_validate(data)
result.append(contract_type)

return result

mock.compile.side_effect = mock_compile
return mock
def mock_compiler(make_mock_compiler):
return make_mock_compiler()


@pytest.fixture
def make_mock_compiler(mocker):
def fn(name="mock"):
mock = mocker.MagicMock()
mock.name = "mock"
mock.ext = f".__{name}__"
mock.tracked_settings = []
mock.ast = None
mock.pcmap = None

def mock_compile(paths, project=None, settings=None):
settings = settings or {}
mock.tracked_settings.append(settings)
result = []
for path in paths:
if path.suffix == mock.ext:
name = path.stem
code = to_hex(123)
data = {
"contractName": name,
"abi": mock.abi,
"deploymentBytecode": code,
"sourceId": f"{project.contracts_folder.name}/{path.name}",
}
if ast := mock.ast:
data["ast"] = ast
if pcmap := mock.pcmap:
data["pcmap"] = pcmap

# Check for mocked overrides
overrides = mock.overrides
if isinstance(overrides, dict):
data = {**data, **overrides}

contract_type = ContractType.model_validate(data)
result.append(contract_type)

return result

mock.compile.side_effect = mock_compile
return mock

return fn


@pytest.fixture
Expand Down
85 changes: 77 additions & 8 deletions tests/functional/test_compilers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,18 @@ def test_flatten_contract(compilers, project_with_contract):
compilers.flatten_contract(Path("contract.foo"))


def test_contract_type_collision(compilers, project_with_contract, mock_compiler):
@pytest.mark.parametrize("factory", (str, Path))
def test_compile(compilers, project_with_contract, factory):
"""
Testing both stringified paths and path-object paths.
"""
path = next(iter(project_with_contract.sources.paths))
actual = compilers.compile((factory(path),))
contract_name = path.stem
assert contract_name in [x.name for x in actual]


def test_compile_contract_type_collision(compilers, project_with_contract, mock_compiler):
_ = compilers.registered_compilers # Ensures cached property is set.

# Hack in our mock compiler.
Expand All @@ -98,13 +109,16 @@ def test_contract_type_collision(compilers, project_with_contract, mock_compiler
del compilers.__dict__["registered_compilers"][mock_compiler.ext]


def test_compile_empty(compilers):
# Also, we are asserting it does no fail.
assert list(compilers.compile([])) == []


def test_compile_with_settings(mock_compiler, compilers, project_with_contract):
new_contract = project_with_contract.path / f"AMockContract{mock_compiler.ext}"
new_contract.write_text("foobar", encoding="utf8")
settings = {"mock": {"foo": "bar"}}

_ = compilers.registered_compilers # Ensures cached property is set.

# Hack in our mock compiler.
compilers.__dict__["registered_compilers"][mock_compiler.ext] = mock_compiler

Expand All @@ -119,11 +133,66 @@ def test_compile_with_settings(mock_compiler, compilers, project_with_contract):
assert actual == settings["mock"]


def test_compile_str_path(compilers, project_with_contract):
path = next(iter(project_with_contract.sources.paths))
actual = compilers.compile((str(path),))
contract_name = path.stem
assert contract_name in [x.name for x in actual]
def test_compile_errors(mock_compiler, compilers, project_with_contract):
new_contract = project_with_contract.path / f"AMockContract{mock_compiler.ext}"
new_contract.write_text("foobar", encoding="utf8")

class MyCustomCompilerError(CompilerError):
pass

mock_compiler.compile.side_effect = MyCustomCompilerError
_ = compilers.registered_compilers # Ensures cached property is set.
# Hack in our mock compiler.
compilers.__dict__["registered_compilers"][mock_compiler.ext] = mock_compiler

try:
with pytest.raises(MyCustomCompilerError):
list(compilers.compile([new_contract], project=project_with_contract))

finally:
if mock_compiler.ext in compilers.__dict__.get("registered_compilers", {}):
del compilers.__dict__["registered_compilers"][mock_compiler.ext]


def test_compile_multiple_errors(
mock_compiler, make_mock_compiler, compilers, project_with_contract
):
"""
Simulating getting errors from multiple compilers.
We should get all the errors.
"""
second_mock_compiler = make_mock_compiler("mock2")
new_contract_0 = project_with_contract.path / f"AMockContract{mock_compiler.ext}"
new_contract_0.write_text("foobar", encoding="utf8")
new_contract_1 = project_with_contract.path / f"AMockContract{second_mock_compiler.ext}"
new_contract_1.write_text("foobar2", encoding="utf8")

expected_0 = "this is expected message 0"
expected_1 = "this is expected message 1"

class MyCustomCompilerError0(CompilerError):
def __init__(self):
super().__init__(expected_0)

class MyCustomCompilerError1(CompilerError):
def __init__(self):
super().__init__(expected_1)

mock_compiler.compile.side_effect = MyCustomCompilerError0
second_mock_compiler.compile.side_effect = MyCustomCompilerError1
_ = compilers.registered_compilers # Ensures cached property is set.
# Hack in our mock compilers.
compilers.__dict__["registered_compilers"][mock_compiler.ext] = mock_compiler
compilers.__dict__["registered_compilers"][second_mock_compiler.ext] = second_mock_compiler

try:
match = rf"{expected_0}\n\n{expected_1}"
with pytest.raises(CompilerError, match=match):
list(compilers.compile([new_contract_0, new_contract_1], project=project_with_contract))

finally:
if mock_compiler.ext in compilers.__dict__.get("registered_compilers", {}):
del compilers.__dict__["registered_compilers"][mock_compiler.ext]


def test_compile_source(compilers):
Expand Down
Loading