ned / coverage.py (http://nedbatchelder.com/code/coverage)
Coverage.py measures Python code coverage, typically during test execution. Kits are at PyPI: http://pypi.python.org/pypi/coverage/.
| commit 685: | b4502b43f653 |
| parent 684: | b5d4b0267ee4 |
| branch: | default |
Changed (Δ3.3 KB):
CHANGES.txt (10 lines added, 1 lines removed)
covcov.ini (1 lines added, 0 lines removed)
coverage/cmdline.py (1 lines added, 1 lines removed)
coverage/config.py (3 lines added, 0 lines removed)
coverage/control.py (18 lines added, 9 lines removed)
coverage/data.py (6 lines added, 4 lines removed)
coverage/misc.py (8 lines added, 0 lines removed)
test/coverage_coverage.py (1 lines added, 2 lines removed)
test/test_api.py (1 lines added, 1 lines removed)
test/test_cmdline.py (21 lines added, 11 lines removed)
test/test_coverage.py (52 lines added, 6 lines removed)
| … | … | @@ -6,7 +6,9 @@ Change history for Coverage.py |
6 |
6 |
Version 3.3 |
7 |
7 |
----------- |
8 |
8 |
|
9 |
- Settings are now read from a .coveragerc file. |
|
9 |
- Settings are now read from a .coveragerc file. The name of the file can be |
|
10 |
set with the `config_file` argument to the coverage() constructor, or reading |
|
11 |
a config file can be disabled with `config_file=False`. |
|
10 |
12 |
|
11 |
13 |
- Fixed a problem with nested loops having their branch possibilities |
12 |
14 |
mischaracterized: `issue 39`_. |
| … | … | @@ -14,9 +16,16 @@ Version 3.3 |
14 |
16 |
- Added coverage.process_start to enable coverage measurement when Python |
15 |
17 |
starts. |
16 |
18 |
|
19 |
- Parallel data file names now have a random number appended to them in |
|
20 |
addition to the machine name and process id. |
|
21 |
||
17 |
22 |
- Parallel data files combined with "coverage combine" are deleted after |
18 |
23 |
they're combined, to clean up unneeded files. Fixes `issue 40`_. |
19 |
24 |
|
25 |
- The `data_suffix` argument to the coverage constructor is now appended with |
|
26 |
an added dot rather than simply appended, so that .coveragerc files will not |
|
27 |
be confused for data files. |
|
28 |
||
20 |
29 |
- Added an AUTHORS.txt file. |
21 |
30 |
|
22 |
31 |
.. _issue 39: http://bitbucket.org/ned/coveragepy/issue/39 |
2 |
2 |
[run] |
3 |
3 |
branch = true |
4 |
4 |
data_file = c:\ned\coverage\trunk\.coverage |
5 |
parallel = true |
|
5 |
6 |
|
6 |
7 |
[report] |
7 |
8 |
exclude_lines = |
Up to file-list coverage/cmdline.py:
| … | … | @@ -427,7 +427,7 @@ class CoverageScript(object): |
427 |
427 |
|
428 |
428 |
# Do something. |
429 |
429 |
self.coverage = self.covpkg.coverage( |
430 |
data_suffix = |
|
430 |
data_suffix = options.parallel_mode, |
|
431 |
431 |
cover_pylib = options.pylib, |
432 |
432 |
timid = options.timid, |
433 |
433 |
branch = options.branch, |
Up to file-list coverage/config.py:
| … | … | @@ -18,6 +18,7 @@ class CoverageConfig(object): |
18 |
18 |
self.branch = False |
19 |
19 |
self.cover_pylib = False |
20 |
20 |
self.data_file = ".coverage" |
21 |
self.parallel = False |
|
21 |
22 |
self.timid = False |
22 |
23 |
|
23 |
24 |
# Defaults for [report] |
| … | … | @@ -56,6 +57,8 @@ class CoverageConfig(object): |
56 |
57 |
self.cover_pylib = cp.getboolean('run', 'cover_pylib') |
57 |
58 |
if cp.has_option('run', 'data_file'): |
58 |
59 |
self.data_file = cp.get('run', 'data_file') |
60 |
if cp.has_option('run', 'parallel'): |
|
61 |
self.parallel = cp.getboolean('run', 'parallel') |
|
59 |
62 |
if cp.has_option('run', 'timid'): |
60 |
63 |
self.timid = cp.getboolean('run', 'timid') |
61 |
64 |
Up to file-list coverage/control.py:
| … | … | @@ -10,6 +10,7 @@ from coverage.config import CoverageConf |
10 |
10 |
from coverage.data import CoverageData |
11 |
11 |
from coverage.files import FileLocator |
12 |
12 |
from coverage.html import HtmlReporter |
13 |
from coverage.misc import bool_or_none |
|
13 |
14 |
from coverage.results import Analysis |
14 |
15 |
from coverage.summary import SummaryReporter |
15 |
16 |
from coverage.xmlreport import XmlReporter |
| … | … | @@ -29,13 +30,13 @@ class coverage(object): |
29 |
30 |
|
30 |
31 |
""" |
31 |
32 |
|
32 |
def __init__(self, data_file=None, data_suffix= |
|
33 |
def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, |
|
33 |
34 |
auto_data=False, timid=None, branch=None, config_file=True): |
34 |
35 |
""" |
35 |
36 |
`data_file` is the base name of the data file to use, defaulting to |
36 |
".coverage". `data_suffix` is appended to `data_file` to create the |
|
37 |
final file name. If `data_suffix` is simply True, then a suffix is |
|
38 |
|
|
37 |
".coverage". `data_suffix` is appended (with a dot) to `data_file` to |
|
38 |
create the final file name. If `data_suffix` is simply True, then a |
|
39 |
suffix is created with the machine and process identity included. |
|
39 |
40 |
|
40 |
41 |
`cover_pylib` is a boolean determining whether Python code installed |
41 |
42 |
with the Python interpreter is measured. This includes the Python |
| … | … | @@ -79,7 +80,7 @@ class coverage(object): |
79 |
80 |
# 4: from constructor arguments: |
80 |
81 |
self.config.from_args( |
81 |
82 |
data_file=data_file, cover_pylib=cover_pylib, timid=timid, |
82 |
branch=branch |
|
83 |
branch=branch, parallel=bool_or_none(data_suffix) |
|
83 |
84 |
) |
84 |
85 |
|
85 |
86 |
self.auto_data = auto_data |
| … | … | @@ -96,11 +97,11 @@ class coverage(object): |
96 |
97 |
) |
97 |
98 |
|
98 |
99 |
# Create the data file. |
99 |
if data_suffix |
|
100 |
if data_suffix or self.config.parallel: |
|
100 |
101 |
if not isinstance(data_suffix, string_class): |
101 |
102 |
# if data_suffix=True, use .machinename.pid.random |
102 |
data_suffix = ".%s.%s.%06d" % ( |
|
103 |
socket.gethostname(), os.getpid(), random.randint(0,999999) |
|
103 |
data_suffix = "%s.%s.%06d" % ( |
|
104 |
socket.gethostname(), os.getpid(), random.randint(0, 999999) |
|
104 |
105 |
) |
105 |
106 |
else: |
106 |
107 |
data_suffix = None |
| … | … | @@ -250,6 +251,14 @@ class coverage(object): |
250 |
251 |
current measurements. |
251 |
252 |
|
252 |
253 |
""" |
254 |
# If the .coveragerc file specifies parallel=True, then self.data |
|
255 |
# already points to a suffixed data file. This won't be right for |
|
256 |
# combining, so make a new self.data with no suffix. |
|
257 |
from coverage import __version__ |
|
258 |
self.data = CoverageData( |
|
259 |
basename=self.config.data_file, |
|
260 |
collector="coverage v%s" % __version__ |
|
261 |
) |
|
253 |
262 |
self.data.combine_parallel_data() |
254 |
263 |
|
255 |
264 |
def _harvest_data(self): |
| … | … | @@ -412,7 +421,7 @@ def process_startup(): |
412 |
421 |
""" |
413 |
422 |
cps = os.environ.get("COVERAGE_PROCESS_START") |
414 |
423 |
if cps: |
415 |
cov = coverage(config_file=cps, auto_data=True |
|
424 |
cov = coverage(config_file=cps, auto_data=True) |
|
416 |
425 |
if os.environ.get("COVERAGE_COVERAGE"): |
417 |
426 |
# Measuring coverage within coverage.py takes yet more trickery. |
418 |
427 |
cov.cover_prefix = "Please measure coverage.py!" |
Up to file-list coverage/data.py:
| … | … | @@ -34,7 +34,8 @@ class CoverageData(object): |
34 |
34 |
|
35 |
35 |
`suffix` is a suffix to append to the base file name. This can be used |
36 |
36 |
for multiple or parallel execution, so that many coverage data files |
37 |
can exist simultaneously. |
|
37 |
can exist simultaneously. A dot will be used to join the base name and |
|
38 |
the suffix. |
|
38 |
39 |
|
39 |
40 |
`collector` is a string describing the coverage measurement software. |
40 |
41 |
|
| … | … | @@ -47,7 +48,7 @@ class CoverageData(object): |
47 |
48 |
# ever do any file storage. |
48 |
49 |
self.filename = basename or ".coverage" |
49 |
50 |
if suffix: |
50 |
self.filename += |
|
51 |
self.filename += "." + suffix |
|
51 |
52 |
self.filename = os.path.abspath(self.filename) |
52 |
53 |
|
53 |
54 |
# A map from canonical Python source file name to a dictionary in |
| … | … | @@ -168,12 +169,13 @@ class CoverageData(object): |
168 |
169 |
"""Combine a number of data files together. |
169 |
170 |
|
170 |
171 |
Treat `self.filename` as a file prefix, and combine the data from all |
171 |
of the data files starting with that prefix |
|
172 |
of the data files starting with that prefix plus a dot. |
|
172 |
173 |
|
173 |
174 |
""" |
174 |
175 |
data_dir, local = os.path.split(self.filename) |
176 |
localdot = local + '.' |
|
175 |
177 |
for f in os.listdir(data_dir or '.'): |
176 |
if f.startswith(local |
|
178 |
if f.startswith(localdot): |
|
177 |
179 |
full_path = os.path.join(data_dir, f) |
178 |
180 |
new_lines, new_arcs = self._read_file(full_path) |
179 |
181 |
for filename, file_data in new_lines.items(): |
Up to file-list coverage/misc.py:
| … | … | @@ -60,6 +60,14 @@ def expensive(fn): |
60 |
60 |
return _wrapped |
61 |
61 |
|
62 |
62 |
|
63 |
def bool_or_none(b): |
|
64 |
"""Return bool(b), but preserve None.""" |
|
65 |
if b is None: |
|
66 |
return None |
|
67 |
else: |
|
68 |
return bool(b) |
|
69 |
||
70 |
||
63 |
71 |
class CoverageException(Exception): |
64 |
72 |
"""An exception specific to Coverage.""" |
65 |
73 |
pass |
Up to file-list test/coverage_coverage.py:
| … | … | @@ -23,7 +23,7 @@ def run_tests_with_coverage(): |
23 |
23 |
|
24 |
24 |
tracer = os.environ.get('COVERAGE_TEST_TRACER', 'c') |
25 |
25 |
version = "%s%s" % sys.version_info[:2] |
26 |
suffix = " |
|
26 |
suffix = "%s_%s" % (version, tracer) |
|
27 |
27 |
|
28 |
28 |
cov = coverage.coverage(config_file="covcov.ini", data_suffix=suffix) |
29 |
29 |
# Cheap trick: the coverage code itself is excluded from measurement, but |
| … | … | @@ -66,7 +66,6 @@ def report_on_combined_files(): |
66 |
66 |
cov = coverage.coverage(config_file="covcov.ini") |
67 |
67 |
cov.combine() |
68 |
68 |
cov.save() |
69 |
||
70 |
69 |
cov.html_report(directory=HTML_DIR) |
71 |
70 |
|
72 |
71 |
Up to file-list test/test_api.py:
| … | … | @@ -238,7 +238,7 @@ class ApiTest(CoverageTest): |
238 |
238 |
""") |
239 |
239 |
|
240 |
240 |
self.assertSameElements(os.listdir("."), ["datatest3.py"]) |
241 |
cov = coverage.coverage(data_file="cov.data", data_suffix=" |
|
241 |
cov = coverage.coverage(data_file="cov.data", data_suffix="14") |
|
242 |
242 |
cov.start() |
243 |
243 |
self.import_module("datatest3") # pragma: recursive coverage |
244 |
244 |
cov.stop() # pragma: recursive coverage |
Up to file-list test/test_cmdline.py:
1 |
1 |
"""Test cmdline.py for coverage.""" |
2 |
2 |
|
3 |
import os, |
|
3 |
import os, pprint, re, shlex, sys, textwrap, unittest |
|
4 |
4 |
import mock |
5 |
5 |
import coverage |
6 |
6 |
|
| … | … | @@ -14,7 +14,7 @@ class CmdLineTest(CoverageTest): |
14 |
14 |
run_in_temp_dir = False |
15 |
15 |
|
16 |
16 |
INIT_LOAD = """\ |
17 |
.coverage(cover_pylib=None, data_suffix= |
|
17 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) |
|
18 |
18 |
.load()\n""" |
19 |
19 |
|
20 |
20 |
def model_object(self): |
| … | … | @@ -47,14 +47,24 @@ class CmdLineTest(CoverageTest): |
47 |
47 |
m2 = self.model_object() |
48 |
48 |
code_obj = compile(code, "<code>", "exec") |
49 |
49 |
eval(code_obj, globals(), { 'm2': m2 }) |
50 |
self.assert |
|
50 |
self.assert_same_method_calls(m1, m2) |
|
51 |
51 |
|
52 |
52 |
def cmd_executes_same(self, args1, args2): |
53 |
53 |
"""Assert that the `args1` executes the same as `args2`.""" |
54 |
54 |
m1, r1 = self.mock_command_line(args1) |
55 |
55 |
m2, r2 = self.mock_command_line(args2) |
56 |
56 |
self.assertEqual(r1, r2) |
57 |
self.assert |
|
57 |
self.assert_same_method_calls(m1, m2) |
|
58 |
||
59 |
def assert_same_method_calls(self, m1, m2): |
|
60 |
"""Assert that `m1.method_calls` and `m2.method_calls` are the same.""" |
|
61 |
# Use a real equality comparison, but if it fails, use a nicer assert |
|
62 |
# so we can tell what's going on. We have to use the real == first due |
|
63 |
# to CmdOptionParser.__eq__ |
|
64 |
if m1.method_calls != m2.method_calls: |
|
65 |
pp1 = pprint.pformat(m1.method_calls) |
|
66 |
pp2 = pprint.pformat(m2.method_calls) |
|
67 |
self.assertMultiLineEqual(pp1+'\n', pp2+'\n') |
|
58 |
68 |
|
59 |
69 |
def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): |
60 |
70 |
"""Run a command line, and check that it prints the right help. |
| … | … | @@ -83,7 +93,7 @@ class ClassicCmdLineTest(CmdLineTest): |
83 |
93 |
def testErase(self): |
84 |
94 |
# coverage -e |
85 |
95 |
self.cmd_executes("-e", """\ |
86 |
.coverage(cover_pylib=None, data_suffix= |
|
96 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) |
|
87 |
97 |
.erase() |
88 |
98 |
""") |
89 |
99 |
self.cmd_executes_same("-e", "--erase") |
| … | … | @@ -93,7 +103,7 @@ class ClassicCmdLineTest(CmdLineTest): |
93 |
103 |
|
94 |
104 |
# -x calls coverage.load first. |
95 |
105 |
self.cmd_executes("-x foo.py", """\ |
96 |
.coverage(cover_pylib=None, data_suffix= |
|
106 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) |
|
97 |
107 |
.load() |
98 |
108 |
.start() |
99 |
109 |
.run_python_file('foo.py', ['foo.py']) |
| … | … | @@ -102,7 +112,7 @@ class ClassicCmdLineTest(CmdLineTest): |
102 |
112 |
""") |
103 |
113 |
# -e -x calls coverage.erase first. |
104 |
114 |
self.cmd_executes("-e -x foo.py", """\ |
105 |
.coverage(cover_pylib=None, data_suffix= |
|
115 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) |
|
106 |
116 |
.erase() |
107 |
117 |
.start() |
108 |
118 |
.run_python_file('foo.py', ['foo.py']) |
| … | … | @@ -111,7 +121,7 @@ class ClassicCmdLineTest(CmdLineTest): |
111 |
121 |
""") |
112 |
122 |
# --timid sets a flag, and program arguments get passed through. |
113 |
123 |
self.cmd_executes("-x --timid foo.py abc 123", """\ |
114 |
.coverage(cover_pylib=None, data_suffix= |
|
124 |
.coverage(cover_pylib=None, data_suffix=None, timid=True, branch=None, config_file=True) |
|
115 |
125 |
.load() |
116 |
126 |
.start() |
117 |
127 |
.run_python_file('foo.py', ['foo.py', 'abc', '123']) |
| … | … | @@ -137,7 +147,7 @@ class ClassicCmdLineTest(CmdLineTest): |
137 |
147 |
def testCombine(self): |
138 |
148 |
# coverage -c |
139 |
149 |
self.cmd_executes("-c", """\ |
140 |
.coverage(cover_pylib=None, data_suffix= |
|
150 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) |
|
141 |
151 |
.load() |
142 |
152 |
.combine() |
143 |
153 |
.save() |
| … | … | @@ -440,7 +450,7 @@ class NewCmdLineTest(CmdLineTest): |
440 |
450 |
self.cmd_executes_same("run --timid f.py", "-e -x --timid f.py") |
441 |
451 |
self.cmd_executes_same("run", "-x") |
442 |
452 |
self.cmd_executes("run --branch foo.py", """\ |
443 |
.coverage(cover_pylib=None, data_suffix= |
|
453 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=True, config_file=True) |
|
444 |
454 |
.erase() |
445 |
455 |
.start() |
446 |
456 |
.run_python_file('foo.py', ['foo.py']) |
| … | … | @@ -448,7 +458,7 @@ class NewCmdLineTest(CmdLineTest): |
448 |
458 |
.save() |
449 |
459 |
""") |
450 |
460 |
self.cmd_executes("run --rcfile=myrc.rc foo.py", """\ |
451 |
.coverage(cover_pylib=None, data_suffix= |
|
461 |
.coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file="myrc.rc") |
|
452 |
462 |
.erase() |
453 |
463 |
.start() |
454 |
464 |
.run_python_file('foo.py', ['foo.py']) |
Up to file-list test/test_coverage.py:
| … | … | @@ -1680,6 +1680,14 @@ class ModuleTest(CoverageTest): |
1680 |
1680 |
class ProcessTest(CoverageTest): |
1681 |
1681 |
"""Tests of the per-process behavior of coverage.py.""" |
1682 |
1682 |
|
1683 |
def number_of_data_files(self): |
|
1684 |
"""Return the number of coverage data files in this directory.""" |
|
1685 |
num = 0 |
|
1686 |
for f in os.listdir('.'): |
|
1687 |
if f.startswith('.coverage.') or f == '.coverage': |
|
1688 |
num += 1 |
|
1689 |
return num |
|
1690 |
||
1683 |
1691 |
def testSaveOnExit(self): |
1684 |
1692 |
self.make_file("mycode.py", """\ |
1685 |
1693 |
h = "Hello" |
| … | … | @@ -1725,18 +1733,56 @@ class ProcessTest(CoverageTest): |
1725 |
1733 |
self.assertFalse(os.path.exists(".coverage")) |
1726 |
1734 |
|
1727 |
1735 |
# After two -p runs, there should be two .coverage.machine.123 files. |
1728 |
self.assertEqual( |
|
1729 |
len([f for f in os.listdir('.') if f.startswith('.coverage')]), |
|
1730 |
|
|
1736 |
self.assertEqual(self.number_of_data_files(), 2) |
|
1731 |
1737 |
|
1732 |
1738 |
# Combine the parallel coverage data files into .coverage . |
1733 |
1739 |
self.run_command("coverage -c") |
1734 |
1740 |
self.assertTrue(os.path.exists(".coverage")) |
1735 |
1741 |
|
1736 |
1742 |
# After combining, there should be only the .coverage file. |
1737 |
self.assertEqual( |
|
1738 |
len([f for f in os.listdir('.') if f.startswith('.coverage')]), |
|
1739 |
|
|
1743 |
self.assertEqual(self.number_of_data_files(), 1) |
|
1744 |
||
1745 |
# Read the coverage file and see that b_or_c.py has all 7 lines |
|
1746 |
# executed. |
|
1747 |
data = coverage.CoverageData() |
|
1748 |
data.read_file(".coverage") |
|
1749 |
self.assertEqual(data.summary()['b_or_c.py'], 7) |
|
1750 |
||
1751 |
def test_combine_with_rc(self): |
|
1752 |
self.make_file("b_or_c.py", """\ |
|
1753 |
import sys |
|
1754 |
a = 1 |
|
1755 |
if sys.argv[1] == 'b': |
|
1756 |
b = 1 |
|
1757 |
else: |
|
1758 |
c = 1 |
|
1759 |
d = 1 |
|
1760 |
print ('done') |
|
1761 |
""") |
|
1762 |
||
1763 |
self.make_file(".coveragerc", """\ |
|
1764 |
[run] |
|
1765 |
parallel = true |
|
1766 |
""") |
|
1767 |
||
1768 |
out = self.run_command("coverage run b_or_c.py b") |
|
1769 |
self.assertEqual(out, 'done\n') |
|
1770 |
self.assertFalse(os.path.exists(".coverage")) |
|
1771 |
||
1772 |
out = self.run_command("coverage run b_or_c.py c") |
|
1773 |
self.assertEqual(out, 'done\n') |
|
1774 |
self.assertFalse(os.path.exists(".coverage")) |
|
1775 |
||
1776 |
# After two runs, there should be two .coverage.machine.123 files. |
|
1777 |
self.assertEqual(self.number_of_data_files(), 2) |
|
1778 |
||
1779 |
# Combine the parallel coverage data files into .coverage . |
|
1780 |
self.run_command("coverage combine") |
|
1781 |
self.assertTrue(os.path.exists(".coverage")) |
|
1782 |
self.assertTrue(os.path.exists(".coveragerc")) |
|
1783 |
||
1784 |
# After combining, there should be only the .coverage file. |
|
1785 |
self.assertEqual(self.number_of_data_files(), 1) |
|
1740 |
1786 |
|
1741 |
1787 |
# Read the coverage file and see that b_or_c.py has all 7 lines |
1742 |
1788 |
# executed. |
