diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2010-01-03 10:23:06 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2010-01-03 10:23:06 -0500 |
commit | a5bc551f78df166a0d0e272fae7a7b5205b416f7 (patch) | |
tree | 652c49d9cf3c401b0e8263764d24d7456f634f89 | |
parent | 47c3f02dab1a745f595008d006f1474f148a9157 (diff) | |
download | python-coveragepy-a5bc551f78df166a0d0e272fae7a7b5205b416f7.tar.gz |
Parallel mode can be set from the .coveragerc file.
-rw-r--r-- | CHANGES.txt | 11 | ||||
-rw-r--r-- | covcov.ini | 1 | ||||
-rw-r--r-- | coverage/cmdline.py | 2 | ||||
-rw-r--r-- | coverage/config.py | 3 | ||||
-rw-r--r-- | coverage/control.py | 27 | ||||
-rw-r--r-- | coverage/data.py | 10 | ||||
-rw-r--r-- | coverage/misc.py | 8 | ||||
-rw-r--r-- | test/coverage_coverage.py | 3 | ||||
-rw-r--r-- | test/test_api.py | 2 | ||||
-rw-r--r-- | test/test_cmdline.py | 32 | ||||
-rw-r--r-- | test/test_coverage.py | 58 |
11 files changed, 122 insertions, 35 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 6bfa891..34c75c8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -6,7 +6,9 @@ Change history for Coverage.py Version 3.3 ----------- -- Settings are now read from a .coveragerc file. +- Settings are now read from a .coveragerc file. The name of the file can be + set with the `config_file` argument to the coverage() constructor, or reading + a config file can be disabled with `config_file=False`. - Fixed a problem with nested loops having their branch possibilities mischaracterized: `issue 39`_. @@ -14,9 +16,16 @@ Version 3.3 - Added coverage.process_start to enable coverage measurement when Python starts. +- Parallel data file names now have a random number appended to them in + addition to the machine name and process id. + - Parallel data files combined with "coverage combine" are deleted after they're combined, to clean up unneeded files. Fixes `issue 40`_. +- The `data_suffix` argument to the coverage constructor is now appended with + an added dot rather than simply appended, so that .coveragerc files will not + be confused for data files. + - Added an AUTHORS.txt file. .. _issue 39: http://bitbucket.org/ned/coveragepy/issue/39 @@ -2,6 +2,7 @@ [run]
branch = true
data_file = c:\ned\coverage\trunk\.coverage
+parallel = true
[report]
exclude_lines =
diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 5f1919d..e82cf27 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -427,7 +427,7 @@ class CoverageScript(object): # Do something. self.coverage = self.covpkg.coverage( - data_suffix = bool(options.parallel_mode), + data_suffix = options.parallel_mode, cover_pylib = options.pylib, timid = options.timid, branch = options.branch, diff --git a/coverage/config.py b/coverage/config.py index 3248177..b8f114a 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -18,6 +18,7 @@ class CoverageConfig(object): self.branch = False self.cover_pylib = False self.data_file = ".coverage" + self.parallel = False self.timid = False # Defaults for [report] @@ -56,6 +57,8 @@ class CoverageConfig(object): self.cover_pylib = cp.getboolean('run', 'cover_pylib') if cp.has_option('run', 'data_file'): self.data_file = cp.get('run', 'data_file') + if cp.has_option('run', 'parallel'): + self.parallel = cp.getboolean('run', 'parallel') if cp.has_option('run', 'timid'): self.timid = cp.getboolean('run', 'timid') diff --git a/coverage/control.py b/coverage/control.py index 18fc2aa..0e60fc5 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -10,6 +10,7 @@ from coverage.config import CoverageConfig from coverage.data import CoverageData from coverage.files import FileLocator from coverage.html import HtmlReporter +from coverage.misc import bool_or_none from coverage.results import Analysis from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -29,13 +30,13 @@ class coverage(object): """ - def __init__(self, data_file=None, data_suffix=False, cover_pylib=None, + def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True): """ `data_file` is the base name of the data file to use, defaulting to - ".coverage". `data_suffix` is appended to `data_file` to create the - final file name. If `data_suffix` is simply True, then a suffix is - created with the machine and process identity included. + ".coverage". `data_suffix` is appended (with a dot) to `data_file` to + create the final file name. If `data_suffix` is simply True, then a + suffix is created with the machine and process identity included. `cover_pylib` is a boolean determining whether Python code installed with the Python interpreter is measured. This includes the Python @@ -79,7 +80,7 @@ class coverage(object): # 4: from constructor arguments: self.config.from_args( data_file=data_file, cover_pylib=cover_pylib, timid=timid, - branch=branch + branch=branch, parallel=bool_or_none(data_suffix) ) self.auto_data = auto_data @@ -96,11 +97,11 @@ class coverage(object): ) # Create the data file. - if data_suffix: + if data_suffix or self.config.parallel: if not isinstance(data_suffix, string_class): # if data_suffix=True, use .machinename.pid.random - data_suffix = ".%s.%s.%06d" % ( - socket.gethostname(), os.getpid(), random.randint(0,999999) + data_suffix = "%s.%s.%06d" % ( + socket.gethostname(), os.getpid(), random.randint(0, 999999) ) else: data_suffix = None @@ -250,6 +251,14 @@ class coverage(object): current measurements. """ + # If the .coveragerc file specifies parallel=True, then self.data + # already points to a suffixed data file. This won't be right for + # combining, so make a new self.data with no suffix. + from coverage import __version__ + self.data = CoverageData( + basename=self.config.data_file, + collector="coverage v%s" % __version__ + ) self.data.combine_parallel_data() def _harvest_data(self): @@ -412,7 +421,7 @@ def process_startup(): """ cps = os.environ.get("COVERAGE_PROCESS_START") if cps: - cov = coverage(config_file=cps, auto_data=True, data_suffix=True) + cov = coverage(config_file=cps, auto_data=True) if os.environ.get("COVERAGE_COVERAGE"): # Measuring coverage within coverage.py takes yet more trickery. cov.cover_prefix = "Please measure coverage.py!" diff --git a/coverage/data.py b/coverage/data.py index f9d0edb..9359af1 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -34,7 +34,8 @@ class CoverageData(object): `suffix` is a suffix to append to the base file name. This can be used for multiple or parallel execution, so that many coverage data files - can exist simultaneously. + can exist simultaneously. A dot will be used to join the base name and + the suffix. `collector` is a string describing the coverage measurement software. @@ -47,7 +48,7 @@ class CoverageData(object): # ever do any file storage. self.filename = basename or ".coverage" if suffix: - self.filename += suffix + self.filename += "." + suffix self.filename = os.path.abspath(self.filename) # A map from canonical Python source file name to a dictionary in @@ -168,12 +169,13 @@ class CoverageData(object): """Combine a number of data files together. Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix. + of the data files starting with that prefix plus a dot. """ data_dir, local = os.path.split(self.filename) + localdot = local + '.' for f in os.listdir(data_dir or '.'): - if f.startswith(local): + if f.startswith(localdot): full_path = os.path.join(data_dir, f) new_lines, new_arcs = self._read_file(full_path) for filename, file_data in new_lines.items(): diff --git a/coverage/misc.py b/coverage/misc.py index 0e6bcf9..4959efe 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -60,6 +60,14 @@ def expensive(fn): return _wrapped +def bool_or_none(b): + """Return bool(b), but preserve None.""" + if b is None: + return None + else: + return bool(b) + + class CoverageException(Exception): """An exception specific to Coverage.""" pass diff --git a/test/coverage_coverage.py b/test/coverage_coverage.py index 5cfef04..1e1cba0 100644 --- a/test/coverage_coverage.py +++ b/test/coverage_coverage.py @@ -23,7 +23,7 @@ def run_tests_with_coverage(): tracer = os.environ.get('COVERAGE_TEST_TRACER', 'c') version = "%s%s" % sys.version_info[:2] - suffix = ".%s_%s" % (version, tracer) + suffix = "%s_%s" % (version, tracer) cov = coverage.coverage(config_file="covcov.ini", data_suffix=suffix) # Cheap trick: the coverage code itself is excluded from measurement, but @@ -66,7 +66,6 @@ def report_on_combined_files(): cov = coverage.coverage(config_file="covcov.ini") cov.combine() cov.save() - cov.html_report(directory=HTML_DIR) diff --git a/test/test_api.py b/test/test_api.py index 69eb649..1f0ad83 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -238,7 +238,7 @@ class ApiTest(CoverageTest): """) self.assertSameElements(os.listdir("."), ["datatest3.py"]) - cov = coverage.coverage(data_file="cov.data", data_suffix=".14") + cov = coverage.coverage(data_file="cov.data", data_suffix="14") cov.start() self.import_module("datatest3") # pragma: recursive coverage cov.stop() # pragma: recursive coverage diff --git a/test/test_cmdline.py b/test/test_cmdline.py index fda282b..9196873 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -1,6 +1,6 @@ """Test cmdline.py for coverage.""" -import os, re, shlex, sys, textwrap, unittest +import os, pprint, re, shlex, sys, textwrap, unittest import mock import coverage @@ -14,7 +14,7 @@ class CmdLineTest(CoverageTest): run_in_temp_dir = False INIT_LOAD = """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) .load()\n""" def model_object(self): @@ -47,14 +47,24 @@ class CmdLineTest(CoverageTest): m2 = self.model_object() code_obj = compile(code, "<code>", "exec") eval(code_obj, globals(), { 'm2': m2 }) - self.assertEqual(m1.method_calls, m2.method_calls) + self.assert_same_method_calls(m1, m2) def cmd_executes_same(self, args1, args2): """Assert that the `args1` executes the same as `args2`.""" m1, r1 = self.mock_command_line(args1) m2, r2 = self.mock_command_line(args2) self.assertEqual(r1, r2) - self.assertEqual(m1.method_calls, m2.method_calls) + self.assert_same_method_calls(m1, m2) + + def assert_same_method_calls(self, m1, m2): + """Assert that `m1.method_calls` and `m2.method_calls` are the same.""" + # Use a real equality comparison, but if it fails, use a nicer assert + # so we can tell what's going on. We have to use the real == first due + # to CmdOptionParser.__eq__ + if m1.method_calls != m2.method_calls: + pp1 = pprint.pformat(m1.method_calls) + pp2 = pprint.pformat(m2.method_calls) + self.assertMultiLineEqual(pp1+'\n', pp2+'\n') def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): """Run a command line, and check that it prints the right help. @@ -83,7 +93,7 @@ class ClassicCmdLineTest(CmdLineTest): def testErase(self): # coverage -e self.cmd_executes("-e", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) .erase() """) self.cmd_executes_same("-e", "--erase") @@ -93,7 +103,7 @@ class ClassicCmdLineTest(CmdLineTest): # -x calls coverage.load first. self.cmd_executes("-x foo.py", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) .load() .start() .run_python_file('foo.py', ['foo.py']) @@ -102,7 +112,7 @@ class ClassicCmdLineTest(CmdLineTest): """) # -e -x calls coverage.erase first. self.cmd_executes("-e -x foo.py", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -111,7 +121,7 @@ class ClassicCmdLineTest(CmdLineTest): """) # --timid sets a flag, and program arguments get passed through. self.cmd_executes("-x --timid foo.py abc 123", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=True, branch=None, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=True, branch=None, config_file=True) .load() .start() .run_python_file('foo.py', ['foo.py', 'abc', '123']) @@ -137,7 +147,7 @@ class ClassicCmdLineTest(CmdLineTest): def testCombine(self): # coverage -c self.cmd_executes("-c", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True) .load() .combine() .save() @@ -440,7 +450,7 @@ class NewCmdLineTest(CmdLineTest): self.cmd_executes_same("run --timid f.py", "-e -x --timid f.py") self.cmd_executes_same("run", "-x") self.cmd_executes("run --branch foo.py", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=True, config_file=True) + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=True, config_file=True) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -448,7 +458,7 @@ class NewCmdLineTest(CmdLineTest): .save() """) self.cmd_executes("run --rcfile=myrc.rc foo.py", """\ - .coverage(cover_pylib=None, data_suffix=False, timid=None, branch=None, config_file="myrc.rc") + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file="myrc.rc") .erase() .start() .run_python_file('foo.py', ['foo.py']) diff --git a/test/test_coverage.py b/test/test_coverage.py index 772075e..f899140 100644 --- a/test/test_coverage.py +++ b/test/test_coverage.py @@ -1680,6 +1680,14 @@ class ModuleTest(CoverageTest): class ProcessTest(CoverageTest): """Tests of the per-process behavior of coverage.py.""" + def number_of_data_files(self): + """Return the number of coverage data files in this directory.""" + num = 0 + for f in os.listdir('.'): + if f.startswith('.coverage.') or f == '.coverage': + num += 1 + return num + def testSaveOnExit(self): self.make_file("mycode.py", """\ h = "Hello" @@ -1725,18 +1733,56 @@ class ProcessTest(CoverageTest): self.assertFalse(os.path.exists(".coverage")) # After two -p runs, there should be two .coverage.machine.123 files. - self.assertEqual( - len([f for f in os.listdir('.') if f.startswith('.coverage')]), - 2) + self.assertEqual(self.number_of_data_files(), 2) # Combine the parallel coverage data files into .coverage . self.run_command("coverage -c") self.assertTrue(os.path.exists(".coverage")) # After combining, there should be only the .coverage file. - self.assertEqual( - len([f for f in os.listdir('.') if f.startswith('.coverage')]), - 1) + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage file and see that b_or_c.py has all 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.summary()['b_or_c.py'], 7) + + def test_combine_with_rc(self): + self.make_file("b_or_c.py", """\ + import sys + a = 1 + if sys.argv[1] == 'b': + b = 1 + else: + c = 1 + d = 1 + print ('done') + """) + + self.make_file(".coveragerc", """\ + [run] + parallel = true + """) + + out = self.run_command("coverage run b_or_c.py b") + self.assertEqual(out, 'done\n') + self.assertFalse(os.path.exists(".coverage")) + + out = self.run_command("coverage run b_or_c.py c") + self.assertEqual(out, 'done\n') + self.assertFalse(os.path.exists(".coverage")) + + # After two runs, there should be two .coverage.machine.123 files. + self.assertEqual(self.number_of_data_files(), 2) + + # Combine the parallel coverage data files into .coverage . + self.run_command("coverage combine") + self.assertTrue(os.path.exists(".coverage")) + self.assertTrue(os.path.exists(".coveragerc")) + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) # Read the coverage file and see that b_or_c.py has all 7 lines # executed. |