Example #1
0
    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)
Example #2
0
    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)
        )
Example #3
0
    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))
Example #4
0
    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))