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/.

Clone this repository (size: 2.3 MB): HTTPS / SSH
$ hg clone http://bitbucket.org/ned/coveragepy/
commit 685: b4502b43f653
parent 684: b5d4b0267ee4
branch: default
Parallel mode can be set from the .coveragerc file.
Ned Batchelder / ned
3 months ago

Changed (Δ3.3 KB):

raw changeset »

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)

Up to file-list CHANGES.txt:

@@ -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

Up to file-list covcov.ini:

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 = bool(options.parallel_mode),
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=False, cover_pylib=None,
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
        created with the machine and process identity included.
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, data_suffix=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 += suffix
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 = ".%s_%s" % (version, tracer)
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=".14")
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, re, shlex, sys, textwrap, unittest
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=False, timid=None, branch=None, config_file=True)
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.assertEqual(m1.method_calls, m2.method_calls)
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.assertEqual(m1.method_calls, m2.method_calls)
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=False, timid=None, branch=None, config_file=True)
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=False, timid=None, branch=None, config_file=True)
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=False, timid=None, branch=None, config_file=True)
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=False, timid=True, branch=None, config_file=True)
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=False, timid=None, branch=None, config_file=True)
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=False, timid=None, branch=True, config_file=True)
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=False, timid=None, branch=None, config_file="myrc.rc")
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
            2)
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
            1)
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.