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

Filter reports by source path or contract name #626

Merged
merged 10 commits into from
Jun 8, 2020
4 changes: 4 additions & 0 deletions brownie/data/default-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ console:
auto_suggest: true
completions: true

reports:
exclude_paths: null
exclude_contracts: null

hypothesis:
deadline: null
max_examples: 50
Expand Down
4 changes: 4 additions & 0 deletions brownie/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,7 @@ class InvalidArgumentWarning(BrownieEnvironmentWarning):

class BrownieTestWarning(Warning):
pass


class BrownieConfigWarning(Warning):
pass
2 changes: 1 addition & 1 deletion brownie/test/managers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def pytest_terminal_summary(self, terminalreporter):

# output coverage report to console
coverage_eval = coverage.get_merged_coverage_eval()
for line in output._build_coverage_output(self.project._build, coverage_eval):
for line in output._build_coverage_output(coverage_eval):
terminalreporter.write_line(line)

# save coverage report as `reports/coverage.json`
Expand Down
112 changes: 90 additions & 22 deletions brownie/test/output.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
#!/usr/bin/python3

import json
import warnings
from pathlib import Path

from brownie._config import CONFIG
from brownie.exceptions import BrownieConfigWarning
from brownie.network.state import TxHistory
from brownie.project import get_loaded_projects
from brownie.utils import color
Expand All @@ -28,8 +31,42 @@ def _save_coverage_report(build, coverage_eval, report_path):
return report_path


def _load_report_exclude_data(settings):
exclude_paths = []
if settings["exclude_paths"]:
exclude = settings["exclude_paths"]
if not isinstance(exclude, list):
exclude = [exclude]
for glob_str in exclude:
if Path(glob_str).is_absolute():
base_path = Path(glob_str).root
else:
base_path = Path(".")
try:
exclude_paths.extend([i.as_posix() for i in base_path.glob(glob_str)])
except Exception:
warnings.warn(
"Invalid glob pattern in config exclude settings: '{glob_str}'",
BrownieConfigWarning,
)

exclude_contracts = []
if settings["exclude_contracts"]:
exclude_contracts = settings["exclude_contracts"]
if not isinstance(exclude_contracts, list):
exclude_contracts = [exclude_contracts]

return exclude_paths, exclude_contracts


def _build_gas_profile_output():
# Formats gas profile report that may be printed to the console
exclude_paths, exclude_contracts = _load_report_exclude_data(CONFIG.settings["reports"])
try:
project = get_loaded_projects()[0]
except IndexError:
project = None

gas = TxHistory().gas_profile
sorted_gas = sorted(gas.items())
grouped_by_contract = {}
Expand All @@ -40,6 +77,14 @@ def _build_gas_profile_output():
for full_name, values in sorted_gas:
contract, function = full_name.split(".", 1)

try:
if project._sources.get_source_path(contract) in exclude_paths:
continue
except (AttributeError, KeyError):
pass
if contract in exclude_contracts:
continue

# calculate padding to get table-like formatting
padding["fn"] = max(padding.get("fn", 0), len(str(function)))
for k, v in values.items():
Expand Down Expand Up @@ -69,28 +114,42 @@ def _build_gas_profile_output():
return lines + [""]


def _build_coverage_output(build, coverage_eval):
def _build_coverage_output(coverage_eval):
# Formats a coverage evaluation report that may be printed to the console
all_totals = [(i._name, _get_totals(i._build, coverage_eval)) for i in get_loaded_projects()]

exclude_paths, exclude_contracts = _load_report_exclude_data(CONFIG.settings["reports"])
all_totals = [
(i, _get_totals(i._build, coverage_eval, exclude_contracts)) for i in get_loaded_projects()
]
all_totals = [i for i in all_totals if i[1]]
lines = []

for project_name, totals in all_totals:
if not totals:
continue
for project, totals in all_totals:

if len(all_totals) > 1:
lines.append(f"\n======== {color('bright magenta')}{project_name}{color} ========")
lines.append(f"\n======== {color('bright magenta')}{project._name}{color} ========")

for name in sorted(totals):
pct = _pct(totals[name]["totals"]["statements"], totals[name]["totals"]["branches"])
for contract_name in sorted(totals):
if project._sources.get_source_path(contract_name) in exclude_paths:
continue

pct = _pct(
totals[contract_name]["totals"]["statements"],
totals[contract_name]["totals"]["branches"],
)
lines.append(
f"\n contract: {color('bright magenta')}{name}{color}"
f"\n contract: {color('bright magenta')}{contract_name}{color}"
f" - {_cov_color(pct)}{pct:.1%}{color}"
)
cov = totals[name]
for fn_name, count in cov["statements"].items():
branch = cov["branches"][fn_name] if fn_name in cov["branches"] else (0, 0, 0)
pct = _pct(count, branch)

cov = totals[contract_name]
results = []
for fn_name, statement_cov in cov["statements"].items():
branch_cov = cov["branches"][fn_name] if fn_name in cov["branches"] else (0, 0, 0)
pct = _pct(statement_cov, branch_cov)
results.append((fn_name, pct))

for fn_name, pct in sorted(results, key=lambda k: (-k[1], k[0])):
lines.append(f" {fn_name} - {_cov_color(pct)}{pct:.1%}{color}")

return lines
Expand All @@ -107,9 +166,11 @@ def _pct(statement, branch):
return pct


def _get_totals(build, coverage_eval):
def _get_totals(build, coverage_eval, exclude_contracts=None):
# Returns a modified coverage eval dict showing counts and totals for each function.

if exclude_contracts is None:
exclude_contracts = []
coverage_eval = _split_by_fn(build, coverage_eval)
results = dict(
(
Expand All @@ -122,19 +183,22 @@ def _get_totals(build, coverage_eval):
)
for i in coverage_eval
)
for name in coverage_eval:
for contract_name in coverage_eval:
if contract_name in exclude_contracts:
del results[contract_name]
continue
try:
coverage_map = build.get(name)["coverageMap"]
coverage_map = build.get(contract_name)["coverageMap"]
except KeyError:
del results[name]
del results[contract_name]
continue

r = results[name]
r = results[contract_name]
r["statements"], r["totals"]["statements"] = _statement_totals(
coverage_eval[name], coverage_map["statements"]
coverage_eval[contract_name], coverage_map["statements"], exclude_contracts
)
r["branches"], r["totals"]["branches"] = _branch_totals(
coverage_eval[name], coverage_map["branches"]
coverage_eval[contract_name], coverage_map["branches"], exclude_contracts
)

return results
Expand Down Expand Up @@ -164,20 +228,24 @@ def _split(coverage_eval, coverage_map, key):
return results


def _statement_totals(coverage_eval, coverage_map):
def _statement_totals(coverage_eval, coverage_map, exclude_contracts):
result = {}
count, total = 0, 0
for path, fn in [(k, x) for k, v in coverage_eval.items() for x in v]:
if fn.split(".")[0] in exclude_contracts:
continue
count += len(coverage_eval[path][fn][0])
total += len(coverage_map[path][fn])
result[fn] = (len(coverage_eval[path][fn][0]), len(coverage_map[path][fn]))
return result, (count, total)


def _branch_totals(coverage_eval, coverage_map):
def _branch_totals(coverage_eval, coverage_map, exclude_contracts):
result = {}
final = [0, 0, 0]
for path, fn in [(k, x) for k, v in coverage_map.items() for x in v]:
if fn.split(".")[0] in exclude_contracts:
continue
if path not in coverage_eval:
true, false = 0, 0
else:
Expand Down
33 changes: 33 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,39 @@ Console

default value: ``true``

.. _config-reports:

Reports
-------

Settings related to reports such as coverage data and gas profiles.

.. py:attribute:: exclude_paths

Paths or `glob patterns <https://en.wikipedia.org/wiki/Glob_%28programming%29>`_ of source files to be excluded from report data.

default value: ``null``

.. code-block:: yaml

reports:
exclude_sources:
- contracts/mocks/**/*.*
- contracts/SafeMath.sol

.. py:attribute:: exclude_contracts

Contract names to be excluded from report data.

default value: ``null``

.. code-block:: yaml

reports:
exclude_contracts:
- SafeMath
- Owned

.. _config-hypothesis:

Hypothesis
Expand Down
31 changes: 11 additions & 20 deletions docs/tests-pytest-intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,6 @@ Once you are finished, type ``quit()`` to continue with the next test.

See :ref:`Inspecting and Debugging Transactions <core-transactions>` for more information on Brownie's debugging functionality.



Evaluating Gas Usage
--------------------

Expand All @@ -403,9 +401,6 @@ When the tests complete, a report will display:
├─ constructor - avg: 211445 low: 211445 high: 211445
└─ set - avg: 21658 low: 21658 high: 21658




Evaluating Coverage
-------------------

Expand All @@ -419,26 +414,22 @@ When the tests complete, a report will display:

::

Coverage analysis:

contract: Token - 82.3%
SafeMath.add - 66.7%
SafeMath.sub - 100.0%
Token.<fallback> - 0.0%
Token.allowance - 100.0%
Token.approve - 100.0%
Token.balanceOf - 100.0%
Token.decimals - 0.0%
Token.name - 100.0%
Token.symbol - 0.0%
Token.totalSupply - 100.0%
Token.transfer - 85.7%
Token.transferFrom - 100.0%
contract: Token - 80.8%
Token.allowance - 100.0%
Token.approve - 100.0%
Token.balanceOf - 100.0%
Token.transfer - 100.0%
Token.transferFrom - 100.0%
SafeMath.add - 75.0%
SafeMath.sub - 75.0%
Token.<fallback> - 0.0%

Coverage report saved at reports/coverage.json

Brownie outputs a % score for each contract method that you can use to quickly gauge your overall coverage level. A detailed coverage report is also saved in the project's ``reports`` folder, that can be viewed via the Brownie GUI. See :ref:`coverage-gui` for more information.

You can exclude specific contracts or source files from this report by modifying your project's :ref:`configuration file <config-reports>`.

.. _xdist:

Using ``xdist`` for Distributed Testing
Expand Down
67 changes: 67 additions & 0 deletions tests/test/test_report_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import pytest

from brownie.exceptions import BrownieConfigWarning
from brownie.test import coverage
from brownie.test.output import _build_coverage_output, _build_gas_profile_output


def test_exclude_gas(tester, ext_tester, config):
tester.useSafeMath(5, 10)
ext_tester.makeExternalCall(tester, 5)

report = _build_gas_profile_output()

config.settings["reports"]["exclude_contracts"] = "ExternalCallTester"
assert _build_gas_profile_output() != report


def test_exclude_gas_as_list(tester, ext_tester, config):
tester.useSafeMath(5, 10)
ext_tester.makeExternalCall(tester, 5)

report = _build_gas_profile_output()

config.settings["reports"]["exclude_contracts"] = ["ExternalCallTester"]
assert _build_gas_profile_output() != report


def test_exclude_gas_internal_calls_no_effect(tester, ext_tester, config):
tester.useSafeMath(5, 10)
ext_tester.makeExternalCall(tester, 5)

report = _build_gas_profile_output()

config.settings["reports"]["exclude_contracts"] = "SafeMath"
assert _build_gas_profile_output() == report


def test_exclude_coverage(coverage_mode, tester, config):
tester.useSafeMath(5, 10)

coverage_eval = coverage.get_merged_coverage_eval()
report = _build_coverage_output(coverage_eval)
assert _build_coverage_output(coverage_eval) == report

config.settings["reports"]["exclude_contracts"] = "SafeMath"
assert _build_coverage_output(coverage_eval) != report


def test_exclude_coverage_by_glob(coverage_mode, tester, vypertester, config):
tester.useSafeMath(5, 10)
vypertester.overflow(1, 2)

coverage_eval = coverage.get_merged_coverage_eval()
report = _build_coverage_output(coverage_eval)
assert _build_coverage_output(coverage_eval) == report

config.settings["reports"]["exclude_paths"] = "contracts/*.vy"
assert _build_coverage_output(coverage_eval) != report


def test_invalid_glob_warns(tester, ext_tester, config):
tester.useSafeMath(5, 10)
ext_tester.makeExternalCall(tester, 5)

config.settings["reports"]["exclude_paths"] = "/contracts"
with pytest.warns(BrownieConfigWarning):
_build_gas_profile_output()