def pytest_sessionfinish(self, session): """ Called after whole test run finished, right before returning the exit status to the system. * Aggregates results from `build/tests-{workerid}.json` files and stores them as `build/test.json`. """ if session.testscollected == 0: raise pytest.UsageError( "xdist workers failed to collect tests. Ensure all test cases are " "isolated with the module_isolation or fn_isolation fixtures.\n\n" "https://eth-brownie.readthedocs.io/en/stable/tests.html#isolating-tests" ) build_path = self.project._build_path # aggregate worker test results report = {"tests": {}, "contracts": self.contracts, "tx": {}} for path in list(build_path.glob("tests-*.json")): with path.open() as fp: data = json.load(fp) assert data["contracts"] == report["contracts"] report["tests"].update(data["tests"]) report["tx"].update(data["tx"]) path.unlink() # store worker coverage results - these are used in `pytest_terminal_summary` for hash_, coverage_eval in report["tx"].items(): coverage._add_transaction(hash_, coverage_eval) # save aggregate test results with build_path.joinpath("tests.json").open("w") as fp: json.dump(report, fp, indent=2, sort_keys=True, default=sorted)
def _expand_trace(self) -> None: """Adds the following attributes to each step of the stack trace: address: The address executing this contract. contractName: The name of the contract. fn: The name of the function. jumpDepth: Number of jumps made since entering this contract. The initial value is 0. source: { filename: path to the source file for this step offset: Start and end offset associated source code } """ if self._trace is not None: return if self._raw_trace is None: self._get_trace() self._trace = trace = self._raw_trace self._new_contracts = [] self._internal_transfers = [] if self.contract_address or not trace: coverage._add_transaction(self.coverage_hash, {}) return if "fn" in trace[0]: return if trace[0]["depth"] == 1: self._trace_origin = "geth" self._call_cost = self.gas_used - trace[0]["gas"] + trace[-1]["gas"] for t in trace: t["depth"] = t["depth"] - 1 else: self._trace_origin = "ganache" self._call_cost = trace[0]["gasCost"] for i in range(len(trace) - 1): trace[i]["gasCost"] = trace[i + 1]["gasCost"] trace[-1]["gasCost"] = 0 # last_map gives a quick reference of previous values at each depth last_map = {0: _get_last_map(self.receiver, self.input[:10])} # type: ignore coverage_eval: Dict = {last_map[0]["name"]: {}} for i in range(len(trace)): # if depth has increased, tx has called into a different contract if trace[i]["depth"] > trace[i - 1]["depth"]: step = trace[i - 1] if step["op"] in ("CREATE", "CREATE2"): # creating a new contract out = next(x for x in trace[i:] if x["depth"] == step["depth"]) address = out["stack"][-1][-40:] sig = f"<{step['op']}>" self._new_contracts.append(EthAddress(address)) if int(step["stack"][-1], 16): self._add_internal_xfer(step["address"], address, step["stack"][-1]) else: # calling an existing contract stack_idx = -4 if step["op"] in ("CALL", "CALLCODE") else -3 offset = int(step["stack"][stack_idx], 16) * 2 sig = HexBytes("".join(step["memory"])[offset : offset + 8]).hex() address = step["stack"][-2][-40:] last_map[trace[i]["depth"]] = _get_last_map(address, sig) coverage_eval.setdefault(last_map[trace[i]["depth"]]["name"], {}) # update trace from last_map last = last_map[trace[i]["depth"]] trace[i].update( address=last["address"], contractName=last["name"], fn=last["fn"][-1], jumpDepth=last["jumpDepth"], source=False, ) if trace[i]["op"] == "CALL" and int(trace[i]["stack"][-3], 16): self._add_internal_xfer( last["address"], trace[i]["stack"][-2][-40:], trace[i]["stack"][-3] ) if not last["pc_map"]: continue pc = last["pc_map"][trace[i]["pc"]] if "path" not in pc: continue trace[i]["source"] = {"filename": last["path_map"][pc["path"]], "offset": pc["offset"]} if "fn" not in pc: continue # calculate coverage 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: coverage_eval[last["name"]][pc["path"]][0].add(pc["statement"]) if "branch" in pc: if pc["op"] != "JUMPI": last["active_branches"].add(pc["branch"]) 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"]) if "active_branches" in last: last["active_branches"].remove(pc["branch"]) # ignore jumps with no function - they are compiler optimizations if "jump" in pc: # jump 'i' is calling into an internal function if pc["jump"] == "i": try: fn = last["pc_map"][trace[i + 1]["pc"]]["fn"] except (KeyError, IndexError): continue if fn != last["fn"][-1]: last["fn"].append(fn) last["jumpDepth"] += 1 # jump 'o' is returning from an internal function elif last["jumpDepth"] > 0: del last["fn"][-1] last["jumpDepth"] -= 1 coverage._add_transaction( self.coverage_hash, dict((k, v) for k, v in coverage_eval.items() if v) )
def _expand_trace(self) -> None: """Adds the following attributes to each step of the stack trace: address: The address executing this contract. contractName: The name of the contract. fn: The name of the function. jumpDepth: Number of jumps made since entering this contract. The initial value is 0. source: { filename: path to the source file for this step offset: Start and end offset associated source code } """ if self._raw_trace is None: self._get_trace() if self._trace is not None: # in case `_get_trace` also expanded the trace, do not repeat return self._trace = trace = self._raw_trace self._new_contracts = [] self._internal_transfers = [] self._subcalls = [] if self.contract_address or not trace: coverage._add_transaction(self.coverage_hash, {}) return if trace[0]["depth"] == 1: self._trace_origin = "geth" self._call_cost = self.gas_used - trace[0]["gas"] + trace[-1]["gas"] for t in trace: t["depth"] = t["depth"] - 1 else: self._trace_origin = "ganache" if trace[0]["gasCost"] >= 21000: # in ganache <6.10.0, gas costs are shifted by one step - we can # identify this when the first step has a gas cost >= 21000 self._call_cost = trace[0]["gasCost"] for i in range(len(trace) - 1): trace[i]["gasCost"] = trace[i + 1]["gasCost"] trace[-1]["gasCost"] = 0 else: self._call_cost = self.gas_used - trace[0]["gas"] + trace[-1][ "gas"] # last_map gives a quick reference of previous values at each depth last_map = { 0: _get_last_map(self.receiver, self.input[:10]) } # type: ignore coverage_eval: Dict = {last_map[0]["name"]: {}} for i in range(len(trace)): # if depth has increased, tx has called into a different contract if trace[i]["depth"] > trace[i - 1]["depth"]: step = trace[i - 1] if step["op"] in ("CREATE", "CREATE2"): # creating a new contract out = next(x for x in trace[i:] if x["depth"] == step["depth"]) address = out["stack"][-1][-40:] sig = f"<{step['op']}>" calldata = None self._new_contracts.append(EthAddress(address)) if int(step["stack"][-1], 16): self._add_internal_xfer(step["address"], address, step["stack"][-1]) else: # calling an existing contract stack_idx = -4 if step["op"] in ("CALL", "CALLCODE") else -3 offset = int(step["stack"][stack_idx], 16) length = int(step["stack"][stack_idx - 1], 16) calldata = HexBytes("".join( step["memory"]))[offset:offset + length] sig = calldata[:4].hex() address = step["stack"][-2][-40:] last_map[trace[i]["depth"]] = _get_last_map(address, sig) coverage_eval.setdefault(last_map[trace[i]["depth"]]["name"], {}) self._subcalls.append({ "from": step["address"], "to": EthAddress(address), "op": step["op"] }) if step["op"] in ("CALL", "CALLCODE"): self._subcalls[-1]["value"] = int(step["stack"][-3], 16) if calldata and last_map[trace[i]["depth"]].get("function"): fn = last_map[trace[i]["depth"]]["function"] zip_ = zip(fn.abi["inputs"], fn.decode_input(calldata)) self._subcalls[-1].update( inputs={i[0]["name"]: i[1] for i in zip_}, # type:ignore function=fn._input_sig, ) elif calldata: self._subcalls[-1]["calldata"] = calldata.hex() # update trace from last_map last = last_map[trace[i]["depth"]] trace[i].update( address=last["address"], contractName=last["name"], fn=last["internal_calls"][-1], jumpDepth=last["jumpDepth"], source=False, ) opcode = trace[i]["op"] if opcode == "CALL" and int(trace[i]["stack"][-3], 16): self._add_internal_xfer(last["address"], trace[i]["stack"][-2][-40:], trace[i]["stack"][-3]) try: pc = last["pc_map"][trace[i]["pc"]] except (KeyError, TypeError): # we don't have enough information about this contract continue if trace[i]["depth"] and opcode in ("RETURN", "REVERT", "INVALID", "SELFDESTRUCT"): subcall: dict = next( i for i in self._subcalls[::-1] if i["to"] == last["address"] # type: ignore ) if opcode == "RETURN": returndata = _get_memory(trace[i], -1) if returndata: fn = last["function"] try: return_values = fn.decode_output(returndata) if len(fn.abi["outputs"]) == 1: return_values = (return_values, ) subcall["return_value"] = return_values except Exception: subcall["returndata"] = returndata.hex() else: subcall["return_value"] = None elif opcode == "SELFDESTRUCT": subcall["selfdestruct"] = True else: if opcode == "REVERT": data = _get_memory(trace[i], -1)[4:] if data: subcall["revert_msg"] = decode_abi(["string"], data)[0] if "revert_msg" not in subcall and "dev" in pc: subcall["revert_msg"] = pc["dev"] if "path" not in pc: continue trace[i]["source"] = { "filename": last["path_map"][pc["path"]], "offset": pc["offset"] } if "fn" not in pc: continue # calculate coverage 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: coverage_eval[last["name"]][pc["path"]][0].add( pc["statement"]) if "branch" in pc: if pc["op"] != "JUMPI": last["active_branches"].add(pc["branch"]) 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"]) if "active_branches" in last: last["active_branches"].remove(pc["branch"]) # ignore jumps with no function - they are compiler optimizations if "jump" in pc: # jump 'i' is calling into an internal function if pc["jump"] == "i": try: fn = last["pc_map"][trace[i + 1]["pc"]]["fn"] except (KeyError, IndexError): continue if fn != last["internal_calls"][-1]: last["internal_calls"].append(fn) last["jumpDepth"] += 1 # jump 'o' is returning from an internal function elif last["jumpDepth"] > 0: del last["internal_calls"][-1] last["jumpDepth"] -= 1 coverage._add_transaction( self.coverage_hash, dict((k, v) for k, v in coverage_eval.items() if v))
def _expand_trace(self) -> None: """Adds the following attributes to each step of the stack trace: address: The address executing this contract. contractName: The name of the contract. fn: The name of the function. jumpDepth: Number of jumps made since entering this contract. The initial value is 0. source: { filename: path to the source file for this step offset: Start and end offset associated source code } """ if self._trace is not None: return if self._raw_trace is None: self._get_trace() self._trace = trace = self._raw_trace if not trace or "fn" in trace[0]: coverage._add_transaction(self.coverage_hash, {}) return # last_map gives a quick reference of previous values at each depth last_map = { 0: _get_last_map(self.receiver, self.input[:10]) } # type: ignore coverage_eval: Dict = {last_map[0]["name"]: {}} active_branches = set() for i in range(len(trace)): # if depth has increased, tx has called into a different contract if trace[i]["depth"] > trace[i - 1]["depth"]: # get call signature stack_idx = -4 if trace[i - 1]["op"] in {"CALL", "CALLCODE" } else -3 offset = int(trace[i - 1]["stack"][stack_idx], 16) * 2 sig = HexBytes("".join(trace[i - 1]["memory"])[offset:offset + 8]).hex() # get contract and method name address = trace[i - 1]["stack"][-2][-40:] last_map[trace[i]["depth"]] = _get_last_map(address, sig) coverage_eval.setdefault(last_map[trace[i]["depth"]]["name"], {}) # update trace from last_map last = last_map[trace[i]["depth"]] trace[i].update({ "address": last["address"], "contractName": last["name"], "fn": last["fn"][-1], "jumpDepth": last["jumpDepth"], "source": False, }) if "pc_map" not in last: continue pc = last["pc_map"][trace[i]["pc"]] if "path" not in pc: continue trace[i]["source"] = { "filename": pc["path"], "offset": pc["offset"] } if "fn" not in pc: continue # calculate coverage if pc["path"] != "<stdin>": if pc["path"] not in coverage_eval[last["name"]]: coverage_eval[last["name"]][pc["path"]] = [ set(), set(), set() ] if "statement" in pc: coverage_eval[last["name"]][pc["path"]][0].add( pc["statement"]) if "branch" in pc: if pc["op"] != "JUMPI": active_branches.add(pc["branch"]) elif pc["branch"] in 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"]) active_branches.remove(pc["branch"]) # ignore jumps with no function - they are compiler optimizations if "jump" not in pc: continue # jump 'i' is calling into an internal function if pc["jump"] == "i": try: fn = last["pc_map"][trace[i + 1]["pc"]]["fn"] except (KeyError, IndexError): continue if fn != last["fn"][-1]: last["fn"].append(fn) last["jumpDepth"] += 1 # jump 'o' is returning from an internal function elif last["jumpDepth"] > 0: del last["fn"][-1] last["jumpDepth"] -= 1 coverage._add_transaction( self.coverage_hash, dict((k, v) for k, v in coverage_eval.items() if v))