Skip to content

Commit

Permalink
fix: more source traceback improvements [APE-1335] (ApeWorX#1636)
Browse files Browse the repository at this point in the history
  • Loading branch information
antazoey authored Aug 29, 2023
1 parent b45bf22 commit 0429b36
Show file tree
Hide file tree
Showing 14 changed files with 109 additions and 64 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
# ** Dependencies maintained by ApeWorX **
"eip712>=0.2.1,<0.3",
"ethpm-types>=0.5.3,<0.6",
"evm-trace>=0.1.0a22",
"evm-trace>=0.1.0a23",
],
entry_points={
"console_scripts": ["ape=ape._cli:cli"],
Expand Down
11 changes: 2 additions & 9 deletions src/ape/api/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,18 +189,11 @@ def _create_contract_from_call(
evm_frame = EvmTraceFrame(**frame.raw)
data = create_call_node_data(evm_frame)
calldata = data.get("calldata", HexBytes(""))

if "address" not in data:
if not (address := (data.get("address", frame.contract_address) or None)):
return None, calldata

# NOTE: Handling when providers give us odd address values.
# NOTE: `or ""` because sometimes the address key exists and is None.
raw_addr = HexBytes(data.get("address") or "").hex().replace("0x", "")
zeroes = max(40 - len(raw_addr), 0) * "0"
addr = f"0x{zeroes}{raw_addr}"

try:
address = self.provider.network.ecosystem.decode_address(addr)
address = self.provider.network.ecosystem.decode_address(address)
except Exception:
return None, calldata

Expand Down
9 changes: 5 additions & 4 deletions src/ape/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1085,11 +1085,10 @@ def _send_call_as_txn(
) -> bytes:
account = self.account_manager.test_accounts[0]
receipt = account.call(txn, **kwargs)
call_tree = receipt.call_tree
if not call_tree:
if not (call_tree := receipt.call_tree):
return self._send_call(txn, **kwargs)

# Grab raw retrurndata before enrichment
# Grab raw returndata before enrichment
returndata = call_tree.outputs

if (track_gas or track_coverage) and show_gas and not show_trace:
Expand Down Expand Up @@ -1454,6 +1453,9 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI:
),
)

# NOTE: Ensure to cache even the failed receipts.
self.chain_manager.history.append(receipt)

if receipt.failed:
txn_dict = receipt.transaction.dict()
txn_params = cast(TxParams, txn_dict)
Expand All @@ -1467,7 +1469,6 @@ def send_transaction(self, txn: TransactionAPI) -> ReceiptAPI:
raise vm_err from err

logger.info(f"Confirmed {receipt.txn_hash} (total fees paid = {receipt.total_fees_paid})")
self.chain_manager.history.append(receipt)
return receipt

def _create_call_tree_node(
Expand Down
5 changes: 2 additions & 3 deletions src/ape/api/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,9 +547,8 @@ def track_coverage(self):
return

tracker = self._test_runner.coverage_tracker
if self.provider.supports_tracing:
traceback = self.source_traceback
if traceback is not None and len(traceback) > 0 and self._test_runner is not None:
if self.provider.supports_tracing and (traceback := self.source_traceback):
if len(traceback) > 0:
tracker.cover(traceback)

elif method := self.method_called:
Expand Down
32 changes: 25 additions & 7 deletions src/ape/managers/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,20 +622,34 @@ def __getitem_str(self, account_or_hash: str) -> Union[AccountHistory, ReceiptAP
:class:`~ape.api.transactions.ReceiptAPI`: The receipt.
"""

try:
address = self.provider.network.ecosystem.decode_address(account_or_hash)
return self._get_account_history(address)
except Exception as err:
# Use Transaction hash
def _get_receipt() -> Optional[ReceiptAPI]:
try:
return self._get_receipt(account_or_hash)
except Exception:
pass
return None

try:
address = self.provider.network.ecosystem.decode_address(account_or_hash)
history = self._get_account_history(address)
if len(history) > 0:
return history

except Exception as err:
# Try to treat as transaction hash.
if receipt := _get_receipt():
return receipt

raise ChainError(
f"'{account_or_hash}' is not a known address or transaction hash."
) from err

# No account history found. Check for transaction hash.
if receipt := _get_receipt():
return receipt

# Nothing found. Return empty history
return history

def _get_receipt(self, txn_hash: str) -> ReceiptAPI:
receipt = self._hash_to_receipt_map.get(txn_hash)
if not receipt:
Expand Down Expand Up @@ -1697,4 +1711,8 @@ def get_receipt(self, transaction_hash: str) -> ReceiptAPI:
Returns:
:class:`~ape.apt.transactions.ReceiptAPI`
"""
return self.chain_manager.history[transaction_hash]
receipt = self.chain_manager.history[transaction_hash]
if not isinstance(receipt, ReceiptAPI):
raise ChainError(f"No receipt found with hash '{transaction_hash}'.")

return receipt
10 changes: 7 additions & 3 deletions src/ape/managers/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,12 @@ def convert(self, value: Any, type: Union[Type, Tuple, List]) -> Any:
try:
return converter.convert(value)
except Exception as err:
raise ConversionError(
f"Failed to convert '{value}' using '{converter.__class__.__name__}'."
) from err
try:
error_value = f" '{value}' "
except Exception:
error_value = " "

message = f"Failed to convert{error_value}using '{converter.__class__.__name__}'."
raise ConversionError(message) from err

raise ConversionError(f"No conversion registered to handle '{value}'.")
5 changes: 2 additions & 3 deletions src/ape/managers/project/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -771,11 +771,10 @@ def track_deployment(self, contract: ContractInstance):
destination.write_text(artifact.json())

def _create_contract_source(self, contract_type: ContractType) -> Optional[ContractSource]:
if not contract_type.source_id:
if not (source_id := contract_type.source_id):
return None

src = self._lookup_source(contract_type.source_id)
if not src:
if not (src := self._lookup_source(source_id)):
return None

try:
Expand Down
19 changes: 13 additions & 6 deletions src/ape/pytest/contextmanagers.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ def _check_dev_message(self, exception: ContractLogicError):
raise AssertionError(str(err)) from err

if dev_message is None:
raise AssertionError("Could not find the source of the revert.")
err_message = "Could not find the source of the revert."

# Attempt to show source traceback so the user can see the real failure.
if (info := self.revert_info) and (ex := info.value):
if tb := ex.source_traceback:
err_message = f"{err_message}\n{tb}"

raise AssertionError(err_message)

if not (
(self.dev_message.match(dev_message) is not None)
Expand Down Expand Up @@ -156,6 +163,11 @@ def __exit__(self, exc_type: Type, exc_value: Exception, traceback) -> bool:
f"However, an exception of type {type(exc_value)} occurred: {exc_value}."
) from exc_value

# Set the exception on the returned info.
# This allows the user to make further assertions on the exception.
if self.revert_info is not None:
self.revert_info.value = exc_value

if self.dev_message is not None:
self._check_dev_message(exc_value)

Expand All @@ -166,11 +178,6 @@ def __exit__(self, exc_type: Type, exc_value: Exception, traceback) -> bool:
# Is a custom error type.
self._check_custom_error(exc_value)

# Set the exception on the returned info.
# This allows the user to make further assertions on the exception.
if self.revert_info is not None:
self.revert_info.value = exc_value

# Returning True causes the expected exception not to get raised
# and the test to pass
return True
4 changes: 1 addition & 3 deletions src/ape/pytest/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,6 @@ def cover(
self, src_path: Path, pcs: Iterable[int], inc_fn_hits: bool = True
) -> Tuple[Set[int], List[str]]:
source_id = str(get_relative_path(src_path.absolute(), self.base_path))

if source_id not in self.report.sources:
# The source is not tracked for coverage.
return set(), []
Expand All @@ -87,8 +86,7 @@ def cover(
if pc < 0:
continue

source_coverage = self.report.get_source_coverage(source_id)
if not source_coverage:
if not (source_coverage := self.report.get_source_coverage(source_id)):
continue

for contract in source_coverage.contracts:
Expand Down
9 changes: 3 additions & 6 deletions src/ape/pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,17 +184,14 @@ def capture(self, transaction_hash: str):
if not receipt:
return

contract_address = receipt.receiver or receipt.contract_address
if not contract_address:
if not (contract_address := (receipt.receiver or receipt.contract_address)):
return

contract_type = self.chain_manager.contracts.get(contract_address)
if not contract_type:
if not (contract_type := self.chain_manager.contracts.get(contract_address)):
# Not an invoke-transaction or a known address
return

source_id = contract_type.source_id or None
if not source_id:
if not (source_id := (contract_type.source_id or None)):
# Not a local or known contract type.
return

Expand Down
42 changes: 31 additions & 11 deletions src/ape/utils/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,18 +261,38 @@ def _parse_verbose_coverage(coverage: "CoverageReport", statement: bool = True)
# It is impossible to really track.
continue

row = (
(
fn.name,
fn.full_name,
f"{fn.lines_valid}",
f"{fn.miss_count}",
f"{round(fn.line_rate * 100, 2)}%",
if fn.name == "__builtin__":
# Create a row per unique type.
builtins = {x.tag for x in fn.statements if x.tag}
for builtin in builtins:
name_chars = [
c
for c in builtin.lower().strip().replace(" ", "_")
if c.isalpha() or c == "_"
]
name = f"__{''.join(name_chars).replace('dev_', '')}__"
miss = (
0
if any(s.hit_count > 0 for s in fn.statements if s.tag == builtin)
else 1
)
rows.append(
tuple((name, name, "1", f"{miss}", "0.0%" if miss else "100.0%"))
)

else:
row = (
(
fn.name,
fn.full_name,
f"{fn.lines_valid}",
f"{fn.miss_count}",
f"{round(fn.line_rate * 100, 2)}%",
)
if statement
else (fn.name, fn.full_name, "✓" if fn.hit_count > 0 else "x")
)
if statement
else (fn.name, fn.full_name, "✓" if fn.hit_count > 0 else "x")
)
rows.append(row)
rows.append(row)

# Handle cases where normal names are duplicated.
# Use full names in this case.
Expand Down
13 changes: 12 additions & 1 deletion src/ape_accounts/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,18 @@ class KeyfileAccount(AccountAPI):
__cached_key: Optional[HexBytes] = None

def __repr__(self):
return f"<{self.__class__.__name__} address={self.address} alias={self.alias}>"
# NOTE: Prevent errors from preventing repr from working.
try:
address_str = f" address={self.address} "
except Exception:
address_str = ""

try:
alias_str = f" alias={self.alias} "
except Exception:
alias_str = ""

return f"<{self.__class__.__name__}{address_str}{alias_str}>"

@property
def alias(self) -> str:
Expand Down
5 changes: 1 addition & 4 deletions src/ape_ethereum/ecosystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,7 @@ def default_transaction_type(self) -> TransactionType:

@classmethod
def decode_address(cls, raw_address: RawAddress) -> AddressType:
raw: Union[str, HexBytes] = (
HexBytes(raw_address) if isinstance(raw_address, int) else raw_address
)
return to_checksum_address(raw)
return to_checksum_address(HexBytes(raw_address)[-20:].rjust(20, b"\x00"))

@classmethod
def encode_address(cls, address: AddressType) -> RawAddress:
Expand Down
7 changes: 4 additions & 3 deletions src/ape_geth/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from evm_trace import CallType, ParityTraceList
from evm_trace import TraceFrame as EvmTraceFrame
from evm_trace import (
create_trace_frames,
get_calltree_from_geth_call_trace,
get_calltree_from_geth_trace,
get_calltree_from_parity_trace,
Expand Down Expand Up @@ -350,8 +351,8 @@ def get_transaction_trace(self, txn_hash: str) -> Iterator[TraceFrame]:
frames = self._stream_request(
"debug_traceTransaction", [txn_hash, {"enableMemory": True}], "result.structLogs.item"
)
for frame in frames:
yield self._create_trace_frame(EvmTraceFrame(**frame))
for frame in create_trace_frames(frames):
yield self._create_trace_frame(frame)

def _get_transaction_trace_using_call_tracer(self, txn_hash: str) -> Dict:
return self._make_request(
Expand Down Expand Up @@ -624,7 +625,7 @@ def send_call(self, txn: TransactionAPI, **kwargs: Any) -> bytes:
def _trace_call(self, arguments: List[Any]) -> Tuple[Dict, Iterator[EvmTraceFrame]]:
result = self._make_request("debug_traceCall", arguments)
trace_data = result.get("structLogs", [])
return result, (EvmTraceFrame(**f) for f in trace_data)
return result, create_trace_frames(trace_data)

def _eth_call(self, arguments: List) -> bytes:
try:
Expand Down

0 comments on commit 0429b36

Please sign in to comment.