diff options
60 files changed, 1374 insertions, 1355 deletions
@@ -6,9 +6,9 @@ ignore = build htmlcov .tox* - distribute_setup.py ez_setup.py mock.py + mock.py *.min.js sample_html sample_html_beta *.so *.pyd - *.zip + *.gz *.zip _build diff --git a/CHANGES.txt b/CHANGES.txt index 0da1d949..c19421eb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,31 +2,89 @@ Change history for Coverage.py ------------------------------ -Version 3.5.4b1 ---------------- +Version 3.6b1 +------------- - Wildcards in ``include=`` and ``omit=`` arguments were not handled properly in reporting functions, though they were when running. Now they are handled - uniformly, closing `issue 163`. **NOTE**: it is possible that your - configurations may now be incorrect. If you use ``include`` or ``omit`` - during reporting, whether on the command line, through the API, or in a - configuration file, please check carefully that you were not relying on the - old broken behavior. + uniformly, closing `issue 143` and `issue 163`. **NOTE**: it is possible + that your configurations may now be incorrect. If you use ``include`` or + ``omit`` during reporting, whether on the command line, through the API, or + in a configuration file, please check carefully that you were not relying on + the old broken behavior. + +- The **report**, **html**, and **xml** commands now accept a ``--fail-under`` + switch that indicates in the exit status whether the coverage percentage was + less than a particular value. Closes `issue 139`_. + +- The reporting functions coverage.report(), coverage.html_report(), and + coverage.xml_report() now all return a float, the total percentage covered + measurement. + +- The HTML report's title can now be set in the configuration file, with the + ``--title`` switch on the command line, or via the API. + +- Embarrassingly, the `[xml] output=' setting in the .coveragerc file simply + didn't work. Now it does. + +- Coverage percentage metrics are now computed slightly differently under + branch coverage. This means that completely unexecuted files will now + correctly have 0% coverage, fixing `issue 156`_. This also means that your + total coverage numbers will generally now be lower if you are measuring + branch coverage. + +- When installing, now in addition to creating a "coverage" command, two new + aliases are also installed. A "coverage2" or "coverage3" command will be + created, depending on whether you are installing in Python 2.x or 3.x. + A "coverage-X.Y" command will also be created corresponding to your specific + version of Python. Closes `issue 111`. + +- The coverage.py installer no longer tries to bootstrap setuptools or + Distribute. You must have one of them installed first, as `issue 202` + recommended. + +- The coverage.py kit now includes docs (closing `issue 137`) and tests. + +- On Windows, files are now reported in their correct case, fixing `issue 89`_ + and `issue 203`_. - Running an HTML report in Python 3 in the same directory as an old Python 2 HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_) is now fixed. +- If `coverage xml` fails because there is no data to report, it used to + create a zero-length XML file. Now it doesn't, fixing `issue 210`_. + - Running coverage under a debugger is unlikely to work, but it shouldn't fail with "TypeError: 'NoneType' object is not iterable". Fixes `issue 201`_. - Docstrings for the legacy singleton methods are more helpful. Thanks Marius Gedminas. Closes `issue 205`_. +- The pydoc tool can now show docmentation for the class `coverage.coverage`. + Closes `issue 206`_. + +- Added a page to the docs about contributing to coverage.py, closing + `issue 171`_. + +- Other minor bugs fixed: `issue 153`_. + +.. _issue 89: https://bitbucket.org/ned/coveragepy/issue/89/on-windows-all-packages-are-reported-in +.. _issue 111: https://bitbucket.org/ned/coveragepy/issue/111/when-installing-coverage-with-pip-not +.. _issue 137: https://bitbucket.org/ned/coveragepy/issue/137/provide-docs-with-source-distribution +.. _issue 139: https://bitbucket.org/ned/coveragepy/issue/139/easy-check-for-a-certain-coverage-in-tests +.. _issue 143: https://bitbucket.org/ned/coveragepy/issue/143/omit-doesnt-seem-to-work-in-coverage +.. _issue 153: https://bitbucket.org/ned/coveragepy/issue/153/non-existent-filename-triggers +.. _issue 156: https://bitbucket.org/ned/coveragepy/issue/156/a-completely-unexecuted-file-shows-14 .. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename +.. _issue 171: https://bitbucket.org/ned/coveragepy/issue/171/how-to-contribute-and-run-tests .. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy .. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on +.. _issue 202: https://bitbucket.org/ned/coveragepy/issue/202/get-rid-of-ez_setuppy-and +.. _issue 203: https://bitbucket.org/ned/coveragepy/issue/203/duplicate-filenames-reported-when-filename .. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly +.. _issue 206: https://bitbucket.org/ned/coveragepy/issue/206/pydoc-coveragecoverage-fails-with-an-error +.. _issue 210: https://bitbucket.org/ned/coveragepy/issue/210/if-theres-no-coverage-data-coverage-xml Version 3.5.3 --- 29 September 2012 diff --git a/MANIFEST.in b/MANIFEST.in index 196db1c4..0150d90e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,12 +1,16 @@ # MANIFEST.in file for coverage.py +recursive-include coverage/htmlfiles * +recursive-include coverage/fullcoverage * + include coverage.egg-info/*.* -include coverage/*.py -include coverage/htmlfiles/*.* -include coverage/fullcoverage/*.* -include distribute_setup.py -include ez_setup.py include setup.py include README.txt include CHANGES.txt include AUTHORS.txt -prune test +include requirements.txt +include igor.py +include tox.ini + +recursive-include test * +recursive-include doc *.rst +global-exclude *.pyc @@ -22,7 +22,10 @@ clean: -rm -f $(TEST_ZIP) -rm -rf test/eggsrc/build test/eggsrc/dist test/eggsrc/*.egg-info -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz - -rm -rf doc/_build/* + -rm -rf doc/_build + +sterile: clean + -rm -rf .tox* LINTABLE = coverage setup.py test diff --git a/coverage/__init__.py b/coverage/__init__.py index 2fc49d0f..0ccc699f 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -5,19 +5,13 @@ http://nedbatchelder.com/code/coverage """ -__version__ = "3.5.4b1" # see detailed history in CHANGES.txt - -__url__ = "http://nedbatchelder.com/code/coverage" -if max(__version__).isalpha(): - # For pre-releases, use a version-specific URL. - __url__ += "/" + __version__ +from coverage.version import __version__, __url__ from coverage.control import coverage, process_startup from coverage.data import CoverageData from coverage.cmdline import main, CoverageScript from coverage.misc import CoverageException - # Module-level functions. The original API to this module was based on # functions defined directly in the module, with a singleton of the coverage() # class. That design hampered programmability, so the current api uses @@ -79,6 +73,22 @@ report = _singleton_method('report') annotate = _singleton_method('annotate') +# On Windows, we encode and decode deep enough that something goes wrong and +# the encodings.utf_8 module is loaded and then unloaded, I don't know why. +# Adding a reference here prevents it from being unloaded. Yuk. +import encodings.utf_8 + +# Because of the "from coverage.control import fooey" lines at the top of the +# file, there's an entry for coverage.coverage in sys.modules, mapped to None. +# This makes some inspection tools (like pydoc) unable to find the class +# coverage.coverage. So remove that entry. +import sys +try: + del sys.modules['coverage.coverage'] +except KeyError: + pass + + # COPYRIGHT AND LICENSE # # Copyright 2001 Gareth Rees. All rights reserved. diff --git a/coverage/__main__.py b/coverage/__main__.py index 111ca2e0..55e0d259 100644 --- a/coverage/__main__.py +++ b/coverage/__main__.py @@ -1,4 +1,4 @@ -"""Coverage.py's main entrypoint.""" +"""Coverage.py's main entry point.""" import sys from coverage.cmdline import main sys.exit(main()) diff --git a/coverage/backward.py b/coverage/backward.py index 637a5976..6347501a 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -49,6 +49,16 @@ try: except NameError: range = range +# A function to iterate listlessly over a dict's items. +if "iteritems" in dir({}): + def iitems(d): + """Produce the items from dict `d`.""" + return d.iteritems() +else: + def iitems(d): + """Produce the items from dict `d`.""" + return d.items() + # Exec is a statement in Py2, a function in Py3 if sys.version_info >= (3, 0): def exec_code_object(code, global_map): @@ -66,12 +76,6 @@ else: ) ) -# ConfigParser was renamed to the more-standard configparser -try: - import configparser -except ImportError: - import ConfigParser as configparser - # Reading Python source and interpreting the coding comment is a big deal. if sys.version_info >= (3, 0): # Python 3.2 provides `tokenize.open`, the best way to open source files. diff --git a/coverage/bytecode.py b/coverage/bytecode.py index 61c311eb..fd5c7da2 100644 --- a/coverage/bytecode.py +++ b/coverage/bytecode.py @@ -27,6 +27,7 @@ class ByteCodes(object): Returns `ByteCode` objects. """ + # pylint: disable=R0924 def __init__(self, code): self.code = code self.offset = 0 diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 1ce5e0f5..5217a313 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -1,6 +1,6 @@ """Command-line support for Coverage.""" -import optparse, re, sys, traceback +import optparse, sys, traceback from coverage.backward import sorted # pylint: disable=W0622 from coverage.execfile import run_python_file, run_python_module @@ -20,10 +20,13 @@ class Opts(object): help="Measure branch coverage in addition to statement coverage." ) directory = optparse.make_option( - '-d', '--directory', action='store', - metavar="DIR", + '-d', '--directory', action='store', metavar="DIR", help="Write the output files to DIR." ) + fail_under = optparse.make_option( + '', '--fail-under', action='store', metavar="MIN", type="int", + help="Exit with a status of 2 if the total coverage is less than MIN." + ) help = optparse.make_option( '-h', '--help', action='store_true', help="Get help on this command." @@ -89,6 +92,10 @@ class Opts(object): help="Use a simpler but slower trace method. Try this if you get " "seemingly impossible results!" ) + title = optparse.make_option( + '', '--title', action='store', metavar="TITLE", + help="A text string to use as the title on the HTML." + ) version = optparse.make_option( '', '--version', action='store_true', help="Display version information and exit." @@ -111,6 +118,7 @@ class CoverageOptionParser(optparse.OptionParser, object): actions=[], branch=None, directory=None, + fail_under=None, help=None, ignore_errors=None, include=None, @@ -122,6 +130,7 @@ class CoverageOptionParser(optparse.OptionParser, object): show_missing=None, source=None, timid=None, + title=None, erase_first=None, version=None, ) @@ -273,9 +282,11 @@ CMDS = { 'html': CmdOptionParser("html", [ Opts.directory, + Opts.fail_under, Opts.ignore_errors, Opts.omit, Opts.include, + Opts.title, ] + GLOBAL_ARGS, usage = "[options] [modules]", description = "Create an HTML report of the coverage of the files. " @@ -285,6 +296,7 @@ CMDS = { 'report': CmdOptionParser("report", [ + Opts.fail_under, Opts.ignore_errors, Opts.omit, Opts.include, @@ -314,20 +326,20 @@ CMDS = { 'xml': CmdOptionParser("xml", [ + Opts.fail_under, Opts.ignore_errors, Opts.omit, Opts.include, Opts.output_xml, ] + GLOBAL_ARGS, cmd = "xml", - defaults = {'outfile': 'coverage.xml'}, usage = "[options] [modules]", description = "Generate an XML report of coverage results." ), } -OK, ERR = 0, 1 +OK, ERR, FAIL_UNDER = 0, 1, 2 class CoverageScript(object): @@ -346,27 +358,10 @@ class CoverageScript(object): self.run_python_file = _run_python_file or run_python_file self.run_python_module = _run_python_module or run_python_module self.help_fn = _help_fn or self.help + self.classic = False self.coverage = None - def help(self, error=None, topic=None, parser=None): - """Display an error message, or the named topic.""" - assert error or topic or parser - if error: - print(error) - print("Use 'coverage help' for help.") - elif parser: - print(parser.format_help().strip()) - else: - # Parse out the topic we want from HELP_TOPICS - topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS) - topics = dict(zip(topic_list[1::2], topic_list[2::2])) - help_msg = topics.get(topic, '').strip() - if help_msg: - print(help_msg % self.covpkg.__dict__) - else: - print("Don't know topic %r" % topic) - def command_line(self, argv): """The bulk of the command line interface to Coverage. @@ -376,15 +371,14 @@ class CoverageScript(object): """ # Collect the command-line options. - if not argv: self.help_fn(topic='minimum_help') return OK # The command syntax we parse depends on the first argument. Classic # syntax always starts with an option. - classic = argv[0].startswith('-') - if classic: + self.classic = argv[0].startswith('-') + if self.classic: parser = ClassicOptionParser() else: parser = CMDS.get(argv[0]) @@ -398,58 +392,12 @@ class CoverageScript(object): if not ok: return ERR - # Handle help. - if options.help: - if classic: - self.help_fn(topic='help') - else: - self.help_fn(parser=parser) - return OK - - if "help" in options.actions: - if args: - for a in args: - parser = CMDS.get(a) - if parser: - self.help_fn(parser=parser) - else: - self.help_fn(topic=a) - else: - self.help_fn(topic='help') - return OK - - # Handle version. - if options.version: - self.help_fn(topic='version') + # Handle help and version. + if self.do_help(options, args, parser): return OK # Check for conflicts and problems in the options. - for i in ['erase', 'execute']: - for j in ['annotate', 'html', 'report', 'combine']: - if (i in options.actions) and (j in options.actions): - self.help_fn("You can't specify the '%s' and '%s' " - "options at the same time." % (i, j)) - return ERR - - if not options.actions: - self.help_fn( - "You must specify at least one of -e, -x, -c, -r, -a, or -b." - ) - return ERR - args_allowed = ( - 'execute' in options.actions or - 'annotate' in options.actions or - 'html' in options.actions or - 'debug' in options.actions or - 'report' in options.actions or - 'xml' in options.actions - ) - if not args_allowed and args: - self.help_fn("Unexpected arguments: %s" % " ".join(args)) - return ERR - - if 'execute' in options.actions and not args: - self.help_fn("Nothing to do.") + if not self.args_ok(options, args): return ERR # Listify the list options. @@ -470,38 +418,7 @@ class CoverageScript(object): ) if 'debug' in options.actions: - if not args: - self.help_fn("What information would you like: data, sys?") - return ERR - for info in args: - if info == 'sys': - print("-- sys ----------------------------------------") - for label, info in self.coverage.sysinfo(): - if info == []: - info = "-none-" - if isinstance(info, list): - print("%15s:" % label) - for e in info: - print("%15s %s" % ("", e)) - else: - print("%15s: %s" % (label, info)) - elif info == 'data': - print("-- data ---------------------------------------") - self.coverage.load() - print("path: %s" % self.coverage.data.filename) - print("has_arcs: %r" % self.coverage.data.has_arcs()) - summary = self.coverage.data.summary(fullpath=True) - if summary: - filenames = sorted(summary.keys()) - print("\n%d files:" % len(filenames)) - for f in filenames: - print("%s: %d lines" % (f, summary[f])) - else: - print("No data collected") - else: - self.help_fn("Don't know what you mean by %r" % info) - return ERR - return OK + return self.do_debug(args) if 'erase' in options.actions or options.erase_first: self.coverage.erase() @@ -509,22 +426,7 @@ class CoverageScript(object): self.coverage.load() if 'execute' in options.actions: - # Run the script. - self.coverage.start() - code_ran = True - try: - try: - if options.module: - self.run_python_module(args[0], args) - else: - self.run_python_file(args[0], args) - except NoSource: - code_ran = False - raise - finally: - if code_ran: - self.coverage.stop() - self.coverage.save() + self.do_execute(options, args) if 'combine' in options.actions: self.coverage.combine() @@ -539,18 +441,165 @@ class CoverageScript(object): ) if 'report' in options.actions: - self.coverage.report( + total = self.coverage.report( show_missing=options.show_missing, **report_args) if 'annotate' in options.actions: self.coverage.annotate( directory=options.directory, **report_args) if 'html' in options.actions: - self.coverage.html_report( - directory=options.directory, **report_args) + total = self.coverage.html_report( + directory=options.directory, title=options.title, + **report_args) if 'xml' in options.actions: outfile = options.outfile - self.coverage.xml_report(outfile=outfile, **report_args) + total = self.coverage.xml_report(outfile=outfile, **report_args) + + if options.fail_under is not None: + if total >= options.fail_under: + return OK + else: + return FAIL_UNDER + else: + return OK + + def help(self, error=None, topic=None, parser=None): + """Display an error message, or the named topic.""" + assert error or topic or parser + if error: + print(error) + print("Use 'coverage help' for help.") + elif parser: + print(parser.format_help().strip()) + else: + help_msg = HELP_TOPICS.get(topic, '').strip() + if help_msg: + print(help_msg % self.covpkg.__dict__) + else: + print("Don't know topic %r" % topic) + + def do_help(self, options, args, parser): + """Deal with help requests. + + Return True if it handled the request, False if not. + + """ + # Handle help. + if options.help: + if self.classic: + self.help_fn(topic='help') + else: + self.help_fn(parser=parser) + return True + + if "help" in options.actions: + if args: + for a in args: + parser = CMDS.get(a) + if parser: + self.help_fn(parser=parser) + else: + self.help_fn(topic=a) + else: + self.help_fn(topic='help') + return True + + # Handle version. + if options.version: + self.help_fn(topic='version') + return True + + return False + + def args_ok(self, options, args): + """Check for conflicts and problems in the options. + + Returns True if everything is ok, or False if not. + + """ + for i in ['erase', 'execute']: + for j in ['annotate', 'html', 'report', 'combine']: + if (i in options.actions) and (j in options.actions): + self.help_fn("You can't specify the '%s' and '%s' " + "options at the same time." % (i, j)) + return False + if not options.actions: + self.help_fn( + "You must specify at least one of -e, -x, -c, -r, -a, or -b." + ) + return False + args_allowed = ( + 'execute' in options.actions or + 'annotate' in options.actions or + 'html' in options.actions or + 'debug' in options.actions or + 'report' in options.actions or + 'xml' in options.actions + ) + if not args_allowed and args: + self.help_fn("Unexpected arguments: %s" % " ".join(args)) + return False + + if 'execute' in options.actions and not args: + self.help_fn("Nothing to do.") + return False + + return True + + def do_execute(self, options, args): + """Implementation of 'coverage run'.""" + + # Run the script. + self.coverage.start() + code_ran = True + try: + try: + if options.module: + self.run_python_module(args[0], args) + else: + self.run_python_file(args[0], args) + except NoSource: + code_ran = False + raise + finally: + self.coverage.stop() + if code_ran: + self.coverage.save() + + def do_debug(self, args): + """Implementation of 'coverage debug'.""" + + if not args: + self.help_fn("What information would you like: data, sys?") + return ERR + for info in args: + if info == 'sys': + print("-- sys ----------------------------------------") + for label, info in self.coverage.sysinfo(): + if info == []: + info = "-none-" + if isinstance(info, list): + print("%15s:" % label) + for e in info: + print("%15s %s" % ("", e)) + else: + print("%15s: %s" % (label, info)) + elif info == 'data': + print("-- data ---------------------------------------") + self.coverage.load() + print("path: %s" % self.coverage.data.filename) + print("has_arcs: %r" % self.coverage.data.has_arcs()) + summary = self.coverage.data.summary(fullpath=True) + if summary: + filenames = sorted(summary.keys()) + print("\n%d files:" % len(filenames)) + for f in filenames: + print("%s: %d lines" % (f, summary[f])) + else: + print("No data collected") + else: + self.help_fn("Don't know what you mean by %r" % info) + return ERR return OK @@ -568,10 +617,10 @@ def unshell_list(s): return s.split(',') -HELP_TOPICS = r""" - -== classic ==================================================================== -Coverage.py version %(__version__)s +HELP_TOPICS = { +# ------------------------- +'classic': +r"""Coverage.py version %(__version__)s Measure, collect, and report on code coverage in Python programs. Usage: @@ -615,8 +664,9 @@ coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] Coverage data is saved in the file .coverage by default. Set the COVERAGE_FILE environment variable to save it somewhere else. - -== help ======================================================================= +""", +# ------------------------- +'help': """\ Coverage.py, version %(__version__)s Measure, collect, and report on code coverage in Python programs. @@ -635,20 +685,22 @@ Commands: Use "coverage help <command>" for detailed help on any command. Use "coverage help classic" for help on older command syntax. For more information, see %(__url__)s - -== minimum_help =============================================================== +""", +# ------------------------- +'minimum_help': """\ Code coverage for Python. Use 'coverage help' for help. - -== version ==================================================================== +""", +# ------------------------- +'version': """\ Coverage.py, version %(__version__)s. %(__url__)s - -""" +""", +} def main(argv=None): - """The main entrypoint to Coverage. + """The main entry point to Coverage. - This is installed as the script entrypoint. + This is installed as the script entry point. """ if argv is None: diff --git a/coverage/collector.py b/coverage/collector.py index b8048e45..1b807b27 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -4,7 +4,7 @@ import os, sys, threading try: # Use the C extension code when we can, for speed. - from coverage.tracer import CTracer + from coverage.tracer import CTracer # pylint: disable=F0401,E0611 except ImportError: # Couldn't import the C extension, maybe it isn't built. if os.getenv('COVERAGE_TEST_TRACER') == 'c': diff --git a/coverage/config.py b/coverage/config.py index 0d1da5f4..8f1f6710 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -1,8 +1,55 @@ """Config file for coverage.py""" -import os -from coverage.backward import configparser # pylint: disable=W0622 -from coverage.backward import string_class # pylint: disable=W0622 +import os, sys +from coverage.backward import string_class, iitems + +# In py3, # ConfigParser was renamed to the more-standard configparser +try: + import configparser # pylint: disable=F0401 +except ImportError: + import ConfigParser as configparser + + +class HandyConfigParser(configparser.ConfigParser): + """Our specialization of ConfigParser.""" + + def read(self, filename): + """Read a filename as UTF-8 configuration data.""" + kwargs = {} + if sys.version_info >= (3, 2): + kwargs['encoding'] = "utf-8" + configparser.ConfigParser.read(self, filename, **kwargs) + + def getlist(self, section, option): + """Read a list of strings. + + The value of `section` and `option` is treated as a comma- and newline- + separated list of strings. Each value is stripped of whitespace. + + Returns the list of strings. + + """ + value_list = self.get(section, option) + values = [] + for value_line in value_list.split('\n'): + for value in value_line.split(','): + value = value.strip() + if value: + values.append(value) + return values + + def getlinelist(self, section, option): + """Read a list of full-line strings. + + The value of `section` and `option` is treated as a newline-separated + list of strings. Each value is stripped of whitespace. + + Returns the list of strings. + + """ + value_list = self.get(section, option) + return list(filter(None, value_list.split('\n'))) + # The default line exclusion regexes DEFAULT_EXCLUDE = [ @@ -30,7 +77,6 @@ class CoverageConfig(object): operation of coverage.py. """ - def __init__(self): """Initialize the configuration attributes to their defaults.""" # Defaults for [run] @@ -54,6 +100,7 @@ class CoverageConfig(object): # Defaults for [html] self.html_dir = "htmlcov" self.extra_css = None + self.html_title = "Coverage report" # Defaults for [xml] self.xml_output = "coverage.xml" @@ -74,101 +121,62 @@ class CoverageConfig(object): def from_args(self, **kwargs): """Read config values from `kwargs`.""" - for k, v in kwargs.items(): + for k, v in iitems(kwargs): if v is not None: if k in self.MUST_BE_LIST and isinstance(v, string_class): v = [v] setattr(self, k, v) - def from_file(self, *files): - """Read configuration from .rc files. + def from_file(self, filename): + """Read configuration from a .rc file. - Each argument in `files` is a file name to read. + `filename` is a file name to read. """ - cp = configparser.RawConfigParser() - cp.read(files) + cp = HandyConfigParser() + cp.read(filename) - # [run] - if cp.has_option('run', 'branch'): - self.branch = cp.getboolean('run', 'branch') - if cp.has_option('run', 'cover_pylib'): - 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', 'include'): - self.include = self.get_list(cp, 'run', 'include') - if cp.has_option('run', 'omit'): - self.omit = self.get_list(cp, 'run', 'omit') - if cp.has_option('run', 'parallel'): - self.parallel = cp.getboolean('run', 'parallel') - if cp.has_option('run', 'source'): - self.source = self.get_list(cp, 'run', 'source') - if cp.has_option('run', 'timid'): - self.timid = cp.getboolean('run', 'timid') + for option_spec in self.CONFIG_FILE_OPTIONS: + self.set_attr_from_config_option(cp, *option_spec) - # [report] - if cp.has_option('report', 'exclude_lines'): - self.exclude_list = \ - self.get_line_list(cp, 'report', 'exclude_lines') - if cp.has_option('report', 'ignore_errors'): - self.ignore_errors = cp.getboolean('report', 'ignore_errors') - if cp.has_option('report', 'include'): - self.include = self.get_list(cp, 'report', 'include') - if cp.has_option('report', 'omit'): - self.omit = self.get_list(cp, 'report', 'omit') - if cp.has_option('report', 'partial_branches'): - self.partial_list = \ - self.get_line_list(cp, 'report', 'partial_branches') - if cp.has_option('report', 'partial_branches_always'): - self.partial_always_list = \ - self.get_line_list(cp, 'report', 'partial_branches_always') - if cp.has_option('report', 'precision'): - self.precision = cp.getint('report', 'precision') - if cp.has_option('report', 'show_missing'): - self.show_missing = cp.getboolean('report', 'show_missing') - - # [html] - if cp.has_option('html', 'directory'): - self.html_dir = cp.get('html', 'directory') - if cp.has_option('html', 'extra_css'): - self.extra_css = cp.get('html', 'extra_css') - - # [xml] - if cp.has_option('xml', 'output'): - self.xml_output = cp.get('xml', 'output') - - # [paths] + # [paths] is special if cp.has_section('paths'): for option in cp.options('paths'): - self.paths[option] = self.get_list(cp, 'paths', option) - - def get_list(self, cp, section, option): - """Read a list of strings from the ConfigParser `cp`. - - The value of `section` and `option` is treated as a comma- and newline- - separated list of strings. Each value is stripped of whitespace. - - Returns the list of strings. - - """ - value_list = cp.get(section, option) - values = [] - for value_line in value_list.split('\n'): - for value in value_line.split(','): - value = value.strip() - if value: - values.append(value) - return values + self.paths[option] = cp.getlist('paths', option) - def get_line_list(self, cp, section, option): - """Read a list of full-line strings from the ConfigParser `cp`. + CONFIG_FILE_OPTIONS = [ + # [run] + ('branch', 'run:branch', 'boolean'), + ('cover_pylib', 'run:cover_pylib', 'boolean'), + ('data_file', 'run:data_file'), + ('include', 'run:include', 'list'), + ('omit', 'run:omit', 'list'), + ('parallel', 'run:parallel', 'boolean'), + ('source', 'run:source', 'list'), + ('timid', 'run:timid', 'boolean'), - The value of `section` and `option` is treated as a newline-separated - list of strings. Each value is stripped of whitespace. + # [report] + ('exclude_list', 'report:exclude_lines', 'linelist'), + ('ignore_errors', 'report:ignore_errors', 'boolean'), + ('include', 'report:include', 'list'), + ('omit', 'report:omit', 'list'), + ('partial_list', 'report:partial_branches', 'linelist'), + ('partial_always_list', 'report:partial_branches_always', 'linelist'), + ('precision', 'report:precision', 'int'), + ('show_missing', 'report:show_missing', 'boolean'), - Returns the list of strings. + # [html] + ('html_dir', 'html:directory'), + ('extra_css', 'html:extra_css'), + ('html_title', 'html:title'), - """ - value_list = cp.get(section, option) - return list(filter(None, value_list.split('\n'))) + # [xml] + ('xml_output', 'xml:output'), + ] + + def set_attr_from_config_option(self, cp, attr, where, type_=''): + """Set an attribute on self if it exists in the ConfigParser.""" + section, option = where.split(":") + if cp.has_option(section, option): + method = getattr(cp, 'get'+type_) + setattr(self, attr, method(section, option)) diff --git a/coverage/control.py b/coverage/control.py index acca99ee..f80e62b6 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -3,7 +3,7 @@ import atexit, os, random, socket, sys from coverage.annotate import AnnotateReporter -from coverage.backward import string_class +from coverage.backward import string_class, iitems from coverage.codeunit import code_unit_factory, CodeUnit from coverage.collector import Collector from coverage.config import CoverageConfig @@ -12,12 +12,13 @@ from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.html import HtmlReporter from coverage.misc import CoverageException, bool_or_none, join_regex +from coverage.misc import file_be_gone from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter class coverage(object): - """Programmatic access to Coverage. + """Programmatic access to coverage.py. To use:: @@ -25,7 +26,7 @@ class coverage(object): cov = coverage() cov.start() - #.. blah blah (run your code) blah blah .. + #.. call your code .. cov.stop() cov.html_report(directory='covhtml') @@ -179,14 +180,6 @@ class coverage(object): # Set the reporting precision. Numbers.set_precision(self.config.precision) - # When tearing down the coverage object, modules can become None. - # Saving the modules as object attributes avoids problems, but it is - # quite ad-hoc which modules need to be saved and which references - # need to use the object attributes. - self.socket = socket - self.os = os - self.random = random - def _canonical_dir(self, f): """Return the canonical directory of the file `f`.""" return os.path.split(self.file_locator.canonical_filename(f))[0] @@ -208,9 +201,6 @@ class coverage(object): should not. """ - if os is None: - return False - if filename.startswith('<'): # Lots of non-file execution is represented with artificial # filenames like "<string>", "<doctest readme.txt[0]>", or @@ -331,7 +321,15 @@ class coverage(object): self.data.read() def start(self): - """Start measuring code coverage.""" + """Start measuring code coverage. + + Coverage measurement actually occurs in functions called after `start` + is invoked. Statements in the same scope as `start` won't be measured. + + Once you invoke `start`, you must also call `stop` eventually, or your + process might not shut down cleanly. + + """ if self.run_suffix: # Calling start() means we're running code, so use the run_suffix # as the data_suffix when we eventually save the data. @@ -362,7 +360,6 @@ class coverage(object): def stop(self): """Stop measuring code coverage.""" self.collector.stop() - self._harvest_data() def erase(self): """Erase previously-collected coverage data. @@ -427,8 +424,8 @@ class coverage(object): # `save()` at the last minute so that the pid will be correct even # if the process forks. data_suffix = "%s.%s.%06d" % ( - self.socket.gethostname(), self.os.getpid(), - self.random.randint(0, 99999) + socket.gethostname(), os.getpid(), + random.randint(0, 99999) ) self._harvest_data() @@ -475,6 +472,7 @@ class coverage(object): # Find files that were never executed at all. for src in self.source: for py_file in find_python_files(src): + py_file = self.file_locator.canonical_filename(py_file) self.data.touch_file(py_file) self._harvested = True @@ -514,6 +512,7 @@ class coverage(object): Returns an `Analysis` object. """ + self._harvest_data() if not isinstance(it, CodeUnit): it = code_unit_factory(it, self.file_locator)[0] @@ -532,13 +531,16 @@ class coverage(object): match those patterns will be included in the report. Modules matching `omit` will not be included in the report. + Returns a float, the total percentage covered. + """ + self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, show_missing=show_missing, ) reporter = SummaryReporter(self, self.config) - reporter.report(morfs, outfile=file) + return reporter.report(morfs, outfile=file) def annotate(self, morfs=None, directory=None, ignore_errors=None, omit=None, include=None): @@ -552,6 +554,7 @@ class coverage(object): See `coverage.report()` for other arguments. """ + self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include ) @@ -559,7 +562,7 @@ class coverage(object): reporter.report(morfs, directory=directory) def html_report(self, morfs=None, directory=None, ignore_errors=None, - omit=None, include=None, extra_css=None): + omit=None, include=None, extra_css=None, title=None): """Generate an HTML report. The HTML is written to `directory`. The file "index.html" is the @@ -569,15 +572,21 @@ class coverage(object): `extra_css` is a path to a file of other CSS to apply on the page. It will be copied into the HTML directory. + `title` is a text string (not HTML) to use as the title of the HTML + report. + See `coverage.report()` for other arguments. + Returns a float, the total percentage covered. + """ + self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, - html_dir=directory, extra_css=extra_css, + html_dir=directory, extra_css=extra_css, html_title=title, ) reporter = HtmlReporter(self, self.config) - reporter.report(morfs) + return reporter.report(morfs) def xml_report(self, morfs=None, outfile=None, ignore_errors=None, omit=None, include=None): @@ -590,12 +599,16 @@ class coverage(object): See `coverage.report()` for other arguments. + Returns a float, the total percentage covered. + """ + self._harvest_data() self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, xml_output=outfile, ) file_to_close = None + delete_file = False if self.config.xml_output: if self.config.xml_output == '-': outfile = sys.stdout @@ -604,10 +617,15 @@ class coverage(object): file_to_close = outfile try: reporter = XmlReporter(self, self.config) - reporter.report(morfs, outfile=outfile) + return reporter.report(morfs, outfile=outfile) + except CoverageException: + delete_file = True + raise finally: if file_to_close: file_to_close.close() + if delete_file: + file_be_gone(self.config.xml_output) def sysinfo(self): """Return a list of (key, value) pairs showing internal information.""" @@ -633,8 +651,8 @@ class coverage(object): ('cwd', os.getcwd()), ('path', sys.path), ('environment', [ - ("%s = %s" % (k, v)) for k, v in os.environ.items() - if re.search("^COV|^PY", k) + ("%s = %s" % (k, v)) for k, v in iitems(os.environ) + if re.search(r"^COV|^PY", k) ]), ] return info diff --git a/coverage/data.py b/coverage/data.py index 7a8d656f..c86a77f2 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -2,8 +2,9 @@ import os -from coverage.backward import pickle, sorted # pylint: disable=W0622 +from coverage.backward import iitems, pickle, sorted # pylint: disable=W0622 from coverage.files import PathAliases +from coverage.misc import file_be_gone class CoverageData(object): @@ -60,10 +61,6 @@ class CoverageData(object): # self.arcs = {} - self.os = os - self.sorted = sorted - self.pickle = pickle - def usefile(self, use_file=True): """Set whether or not to use a disk file for data.""" self.use_file = use_file @@ -93,21 +90,21 @@ class CoverageData(object): def erase(self): """Erase the data, both in this object, and from its file storage.""" if self.use_file: - if self.filename and os.path.exists(self.filename): - os.remove(self.filename) + if self.filename: + file_be_gone(self.filename) self.lines = {} self.arcs = {} def line_data(self): """Return the map from filenames to lists of line numbers executed.""" return dict( - [(f, self.sorted(lmap.keys())) for f, lmap in self.lines.items()] + [(f, sorted(lmap.keys())) for f, lmap in iitems(self.lines)] ) def arc_data(self): """Return the map from filenames to lists of line number pairs.""" return dict( - [(f, self.sorted(amap.keys())) for f, amap in self.arcs.items()] + [(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)] ) def write_file(self, filename): @@ -127,7 +124,7 @@ class CoverageData(object): # Write the pickle to the file. fdata = open(filename, 'wb') try: - self.pickle.dump(data, fdata, 2) + pickle.dump(data, fdata, 2) finally: fdata.close() @@ -159,12 +156,12 @@ class CoverageData(object): # Unpack the 'lines' item. lines = dict([ (f, dict.fromkeys(linenos, None)) - for f, linenos in data.get('lines', {}).items() + for f, linenos in iitems(data.get('lines', {})) ]) # Unpack the 'arcs' item. arcs = dict([ (f, dict.fromkeys(arcpairs, None)) - for f, arcpairs in data.get('arcs', {}).items() + for f, arcpairs in iitems(data.get('arcs', {})) ]) except Exception: pass @@ -187,10 +184,10 @@ class CoverageData(object): 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(): + for filename, file_data in iitems(new_lines): filename = aliases.map(filename) self.lines.setdefault(filename, {}).update(file_data) - for filename, file_data in new_arcs.items(): + for filename, file_data in iitems(new_arcs): filename = aliases.map(filename) self.arcs.setdefault(filename, {}).update(file_data) if f != local: @@ -202,7 +199,7 @@ class CoverageData(object): `line_data` is { filename: { lineno: None, ... }, ...} """ - for filename, linenos in line_data.items(): + for filename, linenos in iitems(line_data): self.lines.setdefault(filename, {}).update(linenos) def add_arc_data(self, arc_data): @@ -211,7 +208,7 @@ class CoverageData(object): `arc_data` is { filename: { (l1,l2): None, ... }, ...} """ - for filename, arcs in arc_data.items(): + for filename, arcs in iitems(arc_data): self.arcs.setdefault(filename, {}).update(arcs) def touch_file(self, filename): @@ -252,8 +249,8 @@ class CoverageData(object): if fullpath: filename_fn = lambda f: f else: - filename_fn = self.os.path.basename - for filename, lines in self.lines.items(): + filename_fn = os.path.basename + for filename, lines in iitems(self.lines): summ[filename_fn(filename)] = len(lines) return summ diff --git a/coverage/execfile.py b/coverage/execfile.py index 3283a3f7..587c2d3c 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -110,7 +110,7 @@ def run_python_file(filename, args, package=None): # We have the source. `compile` still needs the last line to be clean, # so make sure it is, then compile a code object from it. - if source[-1] != '\n': + if not source or source[-1] != '\n': source += '\n' code = compile(source, filename, "exec") diff --git a/coverage/files.py b/coverage/files.py index 632d6e31..40af7bf7 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -2,14 +2,14 @@ from coverage.backward import to_string from coverage.misc import CoverageException -import fnmatch, os, re, sys +import fnmatch, os, os.path, re, sys class FileLocator(object): """Understand how filenames work.""" def __init__(self): # The absolute path to our current directory. - self.relative_dir = abs_file(os.curdir) + os.sep + self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep) # Cache of results of calling the canonical_filename() method, to # avoid duplicating work. @@ -22,8 +22,9 @@ class FileLocator(object): `FileLocator` was constructed. """ - if filename.startswith(self.relative_dir): - filename = filename.replace(self.relative_dir, "", 1) + fnorm = os.path.normcase(filename) + if fnorm.startswith(self.relative_dir): + filename = filename[len(self.relative_dir):] return filename def canonical_filename(self, filename): @@ -74,9 +75,50 @@ class FileLocator(object): return None +if sys.platform == 'win32': + + def actual_path(path): + """Get the actual path of `path`, including the correct case.""" + if path in actual_path.cache: + return actual_path.cache[path] + + head, tail = os.path.split(path) + if not tail: + actpath = head + elif not head: + actpath = tail + else: + head = actual_path(head) + if head in actual_path.list_cache: + files = actual_path.list_cache[head] + else: + try: + files = os.listdir(head) + except OSError: + files = [] + actual_path.list_cache[head] = files + normtail = os.path.normcase(tail) + for f in files: + if os.path.normcase(f) == normtail: + tail = f + break + actpath = os.path.join(head, tail) + actual_path.cache[path] = actpath + return actpath + + actual_path.cache = {} + actual_path.list_cache = {} + +else: + def actual_path(filename): + """The actual path for non-Windows platforms.""" + return filename + def abs_file(filename): """Return the absolute normalized form of `filename`.""" - return os.path.normcase(os.path.abspath(os.path.realpath(filename))) + path = os.path.abspath(os.path.realpath(filename)) + path = actual_path(path) + return path def prep_patterns(patterns): @@ -196,7 +238,7 @@ class PathAliases(object): # either separator. regex_pat = regex_pat.replace(r"\/", r"[\\/]") # We want case-insensitive matching, so add that flag. - regex = re.compile("(?i)" + regex_pat) + regex = re.compile(r"(?i)" + regex_pat) # Normalize the result: it must end with a path separator. result_sep = sep(result) diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py index ad350bc0..6a258d67 100644 --- a/coverage/fullcoverage/encodings.py +++ b/coverage/fullcoverage/encodings.py @@ -37,6 +37,14 @@ class FullCoverageTracer(object): sys.settrace(FullCoverageTracer().fullcoverage_trace) +# In coverage/files.py is actual_filename(), which uses glob.glob. I don't +# understand why, but that use of glob borks everything if fullcoverage is in +# effect. So here we make an ugly hail-mary pass to switch off glob.glob over +# there. This means when using fullcoverage, Windows path names will not be +# their actual case. + +#sys.fullcoverage = True + # Finally, remove our own directory from sys.path; remove ourselves from # sys.modules; and re-import "encodings", which will be the real package # this time. Note that the delete from sys.modules dictionary has to diff --git a/coverage/html.py b/coverage/html.py index 34bf6a61..ed8920f2 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -7,6 +7,7 @@ from coverage.backward import pickle from coverage.misc import CoverageException, Hasher from coverage.phystokens import source_token_lines, source_encoding from coverage.report import Reporter +from coverage.results import Numbers from coverage.templite import Templite # Disable pylint msg W0612, because a bunch of variables look unused, but @@ -46,6 +47,7 @@ class HtmlReporter(Reporter): self.directory = None self.template_globals = { 'escape': escape, + 'title': self.config.html_title, '__url__': coverage.__url__, '__version__': coverage.__version__, } @@ -59,6 +61,7 @@ class HtmlReporter(Reporter): self.arcs = self.coverage.data.has_arcs() self.status = HtmlStatus() self.extra_css = None + self.totals = Numbers() def report(self, morfs): """Generate an HTML report for `morfs`. @@ -94,6 +97,8 @@ class HtmlReporter(Reporter): self.make_local_static_report_files() + return self.totals.pc_covered + def make_local_static_report_files(self): """Make local instances of static files for HTML report.""" # The files we provide must always be copied. @@ -157,7 +162,6 @@ class HtmlReporter(Reporter): nums = analysis.numbers missing_branch_arcs = analysis.missing_branch_arcs() - n_par = 0 # accumulated below. arcs = self.arcs # These classes determine which lines are highlighted by default. @@ -182,7 +186,6 @@ class HtmlReporter(Reporter): line_class.append(c_mis) elif self.arcs and lineno in missing_branch_arcs: line_class.append(c_par) - n_par += 1 annlines = [] for b in missing_branch_arcs[lineno]: if b < 0: @@ -229,7 +232,6 @@ class HtmlReporter(Reporter): # Save this file's information for the index file. index_info = { 'nums': nums, - 'par': n_par, 'html_filename': html_filename, 'name': cu.name, } @@ -245,12 +247,15 @@ class HtmlReporter(Reporter): files = self.files arcs = self.arcs - totals = sum([f['nums'] for f in files]) + self.totals = totals = sum([f['nums'] for f in files]) extra_css = self.extra_css + html = index_tmpl.render(locals()) + if sys.version_info < (3, 0): + html = html.decode("utf-8") self.write_html( os.path.join(self.directory, "index.html"), - index_tmpl.render(locals()) + html ) # Write the latest hashes for next time. @@ -358,5 +363,5 @@ def spaceless(html): Get rid of some. """ - html = re.sub(">\s+<p ", ">\n<p ", html) + html = re.sub(r">\s+<p ", ">\n<p ", html) return html diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html index c6d9eec0..c649a83c 100644 --- a/coverage/htmlfiles/index.html +++ b/coverage/htmlfiles/index.html @@ -2,7 +2,7 @@ <html> <head> <meta http-equiv='Content-Type' content='text/html; charset=utf-8'> - <title>Coverage report</title> + <title>{{ title|escape }}</title> <link rel='stylesheet' href='style.css' type='text/css'> {% if extra_css %} <link rel='stylesheet' href='{{ extra_css }}' type='text/css'> @@ -19,7 +19,7 @@ <div id='header'> <div class='content'> - <h1>Coverage report: + <h1>{{ title|escape }}: <span class='pc_cov'>{{totals.pc_covered_str}}%</span> </h1> <img id='keyboard_icon' src='keybd_closed.png'> @@ -69,7 +69,7 @@ <td>{{totals.n_excluded}}</td> {% if arcs %} <td>{{totals.n_branches}}</td> - <td>{{totals.n_missing_branches}}</td> + <td>{{totals.n_partial_branches}}</td> {% endif %} <td class='right'>{{totals.pc_covered_str}}%</td> </tr> @@ -83,7 +83,7 @@ <td>{{file.nums.n_excluded}}</td> {% if arcs %} <td>{{file.nums.n_branches}}</td> - <td>{{file.nums.n_missing_branches}}</td> + <td>{{file.nums.n_partial_branches}}</td> {% endif %} <td class='right'>{{file.nums.pc_covered_str}}%</td> </tr> diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index 490fad86..525939f8 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -32,7 +32,7 @@ <span class='{{c_mis}} shortkey_m button_toggle_mis'>{{nums.n_missing}} missing</span> <span class='{{c_exc}} shortkey_x button_toggle_exc'>{{nums.n_excluded}} excluded</span> {% if arcs %} - <span class='{{c_par}} shortkey_p button_toggle_par'>{{n_par}} partial</span> + <span class='{{c_par}} shortkey_p button_toggle_par'>{{nums.n_partial_branches}} partial</span> {% endif %} </h2> </div> diff --git a/coverage/misc.py b/coverage/misc.py index fd9be857..3ed854a7 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -1,6 +1,10 @@ """Miscellaneous stuff for Coverage.""" +import errno import inspect +import os +import sys + from coverage.backward import md5, sorted # pylint: disable=W0622 from coverage.backward import string_class, to_bytes @@ -83,6 +87,16 @@ def join_regex(regexes): return "" +def file_be_gone(path): + """Remove a file, and don't get annoyed if it doesn't exist.""" + try: + os.remove(path) + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.ENOENT: + raise + + class Hasher(object): """Hashes Python data into md5.""" def __init__(self): diff --git a/coverage/parser.py b/coverage/parser.py index 636a8353..3694b924 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -529,7 +529,7 @@ class ByteParser(object): chunks.append(chunk) # Give all the chunks a length. - chunks[-1].length = bc.next_offset - chunks[-1].byte + chunks[-1].length = bc.next_offset - chunks[-1].byte # pylint: disable=W0631,C0301 for i in range(len(chunks)-1): chunks[i].length = chunks[i+1].byte - chunks[i].byte @@ -576,7 +576,7 @@ class ByteParser(object): else: # No chunk for this byte! raise Exception("Couldn't find chunk @ %d" % byte) - byte_chunks[byte] = ch + byte_chunks[byte] = ch # pylint: disable=W0631 if ch.line: lines.add(ch.line) diff --git a/coverage/phystokens.py b/coverage/phystokens.py index 3beebab1..166020e1 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -119,7 +119,7 @@ def source_encoding(source): # This is mostly code adapted from Py3.2's tokenize module. - cookie_re = re.compile("coding[:=]\s*([-\w.]+)") + cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)") # Do this so the detect_encode code we copied will work. readline = iter(source.splitlines()).next diff --git a/coverage/results.py b/coverage/results.py index d7e2a9d1..ae22e1c3 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -2,7 +2,7 @@ import os -from coverage.backward import set, sorted # pylint: disable=W0622 +from coverage.backward import iitems, set, sorted # pylint: disable=W0622 from coverage.misc import format_lines, join_regex, NoSource from coverage.parser import CodeParser @@ -41,11 +41,12 @@ class Analysis(object): ) n_branches = self.total_branches() mba = self.missing_branch_arcs() - n_missing_branches = sum( - [len(v) for k,v in mba.items() if k not in self.missing] + n_partial_branches = sum( + [len(v) for k,v in iitems(mba) if k not in self.missing] ) + n_missing_branches = sum([len(v) for k,v in iitems(mba)]) else: - n_branches = n_missing_branches = 0 + n_branches = n_partial_branches = n_missing_branches = 0 self.no_branch = set() self.numbers = Numbers( @@ -54,6 +55,7 @@ class Analysis(object): n_excluded=len(self.excluded), n_missing=len(self.missing), n_branches=n_branches, + n_partial_branches=n_partial_branches, n_missing_branches=n_missing_branches, ) @@ -109,7 +111,7 @@ class Analysis(object): def branch_lines(self): """Returns a list of line numbers that have more than one exit.""" exit_counts = self.parser.exit_counts() - return [l1 for l1,count in exit_counts.items() if count > 1] + return [l1 for l1,count in iitems(exit_counts) if count > 1] def total_branches(self): """How many total branches are there?""" @@ -166,13 +168,14 @@ class Numbers(object): _near100 = 99.0 def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, - n_branches=0, n_missing_branches=0 + n_branches=0, n_partial_branches=0, n_missing_branches=0 ): self.n_files = n_files self.n_statements = n_statements self.n_excluded = n_excluded self.n_missing = n_missing self.n_branches = n_branches + self.n_partial_branches = n_partial_branches self.n_missing_branches = n_missing_branches def set_precision(cls, precision): @@ -236,8 +239,12 @@ class Numbers(object): nums.n_excluded = self.n_excluded + other.n_excluded nums.n_missing = self.n_missing + other.n_missing nums.n_branches = self.n_branches + other.n_branches - nums.n_missing_branches = (self.n_missing_branches + - other.n_missing_branches) + nums.n_partial_branches = ( + self.n_partial_branches + other.n_partial_branches + ) + nums.n_missing_branches = ( + self.n_missing_branches + other.n_missing_branches + ) return nums def __radd__(self, other): diff --git a/coverage/summary.py b/coverage/summary.py index c8fa5be4..4b1cd14e 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -29,7 +29,7 @@ class SummaryReporter(Reporter): header = (fmt_name % "Name") + " Stmts Miss" fmt_coverage = fmt_name + "%6d %6d" if self.branches: - header += " Branch BrPart" + header += " Branch BrMiss" fmt_coverage += " %6d %6d" width100 = Numbers.pc_str_width() header += "%*s" % (width100+4, "Cover") @@ -82,3 +82,5 @@ class SummaryReporter(Reporter): if self.config.show_missing: args += ("",) outfile.write(fmt_coverage % args) + + return total.pc_covered diff --git a/coverage/tracer.c b/coverage/tracer.c index 7bc07ea2..97dd113b 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -235,10 +235,8 @@ CTracer_record_pair(CTracer *self, int l1, int l2) { int ret = RET_OK; - PyObject * t = PyTuple_New(2); + PyObject * t = Py_BuildValue("(ii)", l1, l2); if (t != NULL) { - PyTuple_SET_ITEM(t, 0, MyInt_FromLong(l1)); - PyTuple_SET_ITEM(t, 1, MyInt_FromLong(l2)); if (PyDict_SetItem(self->cur_file_data, t, Py_None) < 0) { STATS( self->stats.errors++; ) ret = RET_ERROR; diff --git a/coverage/version.py b/coverage/version.py new file mode 100644 index 00000000..cf18d62f --- /dev/null +++ b/coverage/version.py @@ -0,0 +1,9 @@ +"""The version and URL for coverage.py""" +# This file is exec'ed in setup.py, don't import anything! + +__version__ = "3.6b1" # see detailed history in CHANGES.txt + +__url__ = "http://nedbatchelder.com/code/coverage" +if max(__version__).isalpha(): + # For pre-releases, use a version-specific URL. + __url__ += "/" + __version__ diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 82db8213..fc16f100 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -84,6 +84,9 @@ class XmlReporter(Reporter): # Use the DOM to write the output file. outfile.write(self.xml_out.toprettyxml()) + # Return the total percentage. + return 100.0 * (lhits_tot + bhits_tot) / (lnum_tot + bnum_tot) + def xml_file(self, cu, analysis): """Add to the XML report for a single file.""" diff --git a/distribute_setup.py b/distribute_setup.py deleted file mode 100644 index 347c7ac1..00000000 --- a/distribute_setup.py +++ /dev/null @@ -1,485 +0,0 @@ -#!python -"""Bootstrap distribute installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from distribute_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import os -import sys -import time -import fnmatch -import tempfile -import tarfile -from distutils import log - -try: - from site import USER_SITE -except ImportError: - USER_SITE = None - -try: - import subprocess - - def _python_cmd(*args): - args = (sys.executable,) + args - return subprocess.call(args) == 0 - -except ImportError: - # will be used for python 2.3 - def _python_cmd(*args): - args = (sys.executable,) + args - # quoting arguments if windows - if sys.platform == 'win32': - def quote(arg): - if ' ' in arg: - return '"%s"' % arg - return arg - args = [quote(arg) for arg in args] - return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 - -DEFAULT_VERSION = "0.6.27" -DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" -SETUPTOOLS_FAKED_VERSION = "0.6c11" - -SETUPTOOLS_PKG_INFO = """\ -Metadata-Version: 1.0 -Name: setuptools -Version: %s -Summary: xxxx -Home-page: xxx -Author: xxx -Author-email: xxx -License: xxx -Description: xxx -""" % SETUPTOOLS_FAKED_VERSION - - -def _install(tarball): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # installing - log.warn('Installing Distribute') - if not _python_cmd('setup.py', 'install'): - log.warn('Something went wrong during the installation.') - log.warn('See the error message above.') - finally: - os.chdir(old_wd) - - -def _build_egg(egg, tarball, to_dir): - # extracting the tarball - tmpdir = tempfile.mkdtemp() - log.warn('Extracting in %s', tmpdir) - old_wd = os.getcwd() - try: - os.chdir(tmpdir) - tar = tarfile.open(tarball) - _extractall(tar) - tar.close() - - # going in the directory - subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) - os.chdir(subdir) - log.warn('Now working in %s', subdir) - - # building an egg - log.warn('Building a Distribute egg in %s', to_dir) - _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) - - finally: - os.chdir(old_wd) - # returning the result - log.warn(egg) - if not os.path.exists(egg): - raise IOError('Could not build the egg.') - - -def _do_download(version, download_base, to_dir, download_delay): - egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' - % (version, sys.version_info[0], sys.version_info[1])) - if not os.path.exists(egg): - tarball = download_setuptools(version, download_base, - to_dir, download_delay) - _build_egg(egg, tarball, to_dir) - sys.path.insert(0, egg) - import setuptools - setuptools.bootstrap_install_from = egg - - -def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, download_delay=15, no_fake=True): - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - was_imported = 'pkg_resources' in sys.modules or \ - 'setuptools' in sys.modules - try: - try: - import pkg_resources - if not hasattr(pkg_resources, '_distribute'): - if not no_fake: - _fake_setuptools() - raise ImportError - except ImportError: - return _do_download(version, download_base, to_dir, download_delay) - try: - pkg_resources.require("distribute>="+version) - return - except pkg_resources.VersionConflict: - e = sys.exc_info()[1] - if was_imported: - sys.stderr.write( - "The required version of distribute (>=%s) is not available,\n" - "and can't be installed while this script is running. Please\n" - "install a more recent version first, using\n" - "'easy_install -U distribute'." - "\n\n(Currently using %r)\n" % (version, e.args[0])) - sys.exit(2) - else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return _do_download(version, download_base, to_dir, - download_delay) - except pkg_resources.DistributionNotFound: - return _do_download(version, download_base, to_dir, - download_delay) - finally: - if not no_fake: - _create_fake_setuptools_pkg_info(to_dir) - -def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, - to_dir=os.curdir, delay=15): - """Download distribute from a specified location and return its filename - - `version` should be a valid distribute version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download - attempt. - """ - # making sure we use the absolute path - to_dir = os.path.abspath(to_dir) - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - tgz_name = "distribute-%s.tar.gz" % version - url = download_base + tgz_name - saveto = os.path.join(to_dir, tgz_name) - src = dst = None - if not os.path.exists(saveto): # Avoid repeated downloads - try: - log.warn("Downloading %s", url) - src = urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = src.read() - dst = open(saveto, "wb") - dst.write(data) - finally: - if src: - src.close() - if dst: - dst.close() - return os.path.realpath(saveto) - -def _no_sandbox(function): - def __no_sandbox(*args, **kw): - try: - from setuptools.sandbox import DirectorySandbox - if not hasattr(DirectorySandbox, '_old'): - def violation(*args): - pass - DirectorySandbox._old = DirectorySandbox._violation - DirectorySandbox._violation = violation - patched = True - else: - patched = False - except ImportError: - patched = False - - try: - return function(*args, **kw) - finally: - if patched: - DirectorySandbox._violation = DirectorySandbox._old - del DirectorySandbox._old - - return __no_sandbox - -def _patch_file(path, content): - """Will backup the file then patch it""" - existing_content = open(path).read() - if existing_content == content: - # already patched - log.warn('Already patched.') - return False - log.warn('Patching...') - _rename_path(path) - f = open(path, 'w') - try: - f.write(content) - finally: - f.close() - return True - -_patch_file = _no_sandbox(_patch_file) - -def _same_content(path, content): - return open(path).read() == content - -def _rename_path(path): - new_name = path + '.OLD.%s' % time.time() - log.warn('Renaming %s into %s', path, new_name) - os.rename(path, new_name) - return new_name - -def _remove_flat_installation(placeholder): - if not os.path.isdir(placeholder): - log.warn('Unkown installation at %s', placeholder) - return False - found = False - for file in os.listdir(placeholder): - if fnmatch.fnmatch(file, 'setuptools*.egg-info'): - found = True - break - if not found: - log.warn('Could not locate setuptools*.egg-info') - return - - log.warn('Removing elements out of the way...') - pkg_info = os.path.join(placeholder, file) - if os.path.isdir(pkg_info): - patched = _patch_egg_dir(pkg_info) - else: - patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) - - if not patched: - log.warn('%s already patched.', pkg_info) - return False - # now let's move the files out of the way - for element in ('setuptools', 'pkg_resources.py', 'site.py'): - element = os.path.join(placeholder, element) - if os.path.exists(element): - _rename_path(element) - else: - log.warn('Could not find the %s element of the ' - 'Setuptools distribution', element) - return True - -_remove_flat_installation = _no_sandbox(_remove_flat_installation) - -def _after_install(dist): - log.warn('After install bootstrap.') - placeholder = dist.get_command_obj('install').install_purelib - _create_fake_setuptools_pkg_info(placeholder) - -def _create_fake_setuptools_pkg_info(placeholder): - if not placeholder or not os.path.exists(placeholder): - log.warn('Could not find the install location') - return - pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) - setuptools_file = 'setuptools-%s-py%s.egg-info' % \ - (SETUPTOOLS_FAKED_VERSION, pyver) - pkg_info = os.path.join(placeholder, setuptools_file) - if os.path.exists(pkg_info): - log.warn('%s already exists', pkg_info) - return - - log.warn('Creating %s', pkg_info) - f = open(pkg_info, 'w') - try: - f.write(SETUPTOOLS_PKG_INFO) - finally: - f.close() - - pth_file = os.path.join(placeholder, 'setuptools.pth') - log.warn('Creating %s', pth_file) - f = open(pth_file, 'w') - try: - f.write(os.path.join(os.curdir, setuptools_file)) - finally: - f.close() - -_create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) - -def _patch_egg_dir(path): - # let's check if it's already patched - pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') - if os.path.exists(pkg_info): - if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): - log.warn('%s already patched.', pkg_info) - return False - _rename_path(path) - os.mkdir(path) - os.mkdir(os.path.join(path, 'EGG-INFO')) - pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') - f = open(pkg_info, 'w') - try: - f.write(SETUPTOOLS_PKG_INFO) - finally: - f.close() - return True - -_patch_egg_dir = _no_sandbox(_patch_egg_dir) - -def _before_install(): - log.warn('Before install bootstrap.') - _fake_setuptools() - - -def _under_prefix(location): - if 'install' not in sys.argv: - return True - args = sys.argv[sys.argv.index('install')+1:] - for index, arg in enumerate(args): - for option in ('--root', '--prefix'): - if arg.startswith('%s=' % option): - top_dir = arg.split('root=')[-1] - return location.startswith(top_dir) - elif arg == option: - if len(args) > index: - top_dir = args[index+1] - return location.startswith(top_dir) - if arg == '--user' and USER_SITE is not None: - return location.startswith(USER_SITE) - return True - - -def _fake_setuptools(): - log.warn('Scanning installed packages') - try: - import pkg_resources - except ImportError: - # we're cool - log.warn('Setuptools or Distribute does not seem to be installed.') - return - ws = pkg_resources.working_set - try: - setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', - replacement=False)) - except TypeError: - # old distribute API - setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) - - if setuptools_dist is None: - log.warn('No setuptools distribution found') - return - # detecting if it was already faked - setuptools_location = setuptools_dist.location - log.warn('Setuptools installation detected at %s', setuptools_location) - - # if --root or --preix was provided, and if - # setuptools is not located in them, we don't patch it - if not _under_prefix(setuptools_location): - log.warn('Not patching, --root or --prefix is installing Distribute' - ' in another location') - return - - # let's see if its an egg - if not setuptools_location.endswith('.egg'): - log.warn('Non-egg installation') - res = _remove_flat_installation(setuptools_location) - if not res: - return - else: - log.warn('Egg installation') - pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') - if (os.path.exists(pkg_info) and - _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): - log.warn('Already patched.') - return - log.warn('Patching...') - # let's create a fake egg replacing setuptools one - res = _patch_egg_dir(setuptools_location) - if not res: - return - log.warn('Patched done.') - _relaunch() - - -def _relaunch(): - log.warn('Relaunching...') - # we have to relaunch the process - # pip marker to avoid a relaunch bug - if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: - sys.argv[0] = 'setup.py' - args = [sys.executable] + sys.argv - sys.exit(subprocess.call(args)) - - -def _extractall(self, path=".", members=None): - """Extract all members from the archive to the current working - directory and set owner, modification time and permissions on - directories afterwards. `path' specifies a different directory - to extract to. `members' is optional and must be a subset of the - list returned by getmembers(). - """ - import copy - import operator - from tarfile import ExtractError - directories = [] - - if members is None: - members = self - - for tarinfo in members: - if tarinfo.isdir(): - # Extract directories with a safe mode. - directories.append(tarinfo) - tarinfo = copy.copy(tarinfo) - tarinfo.mode = 448 # decimal for oct 0700 - self.extract(tarinfo, path) - - # Reverse sort directories. - if sys.version_info < (2, 4): - def sorter(dir1, dir2): - return cmp(dir1.name, dir2.name) - directories.sort(sorter) - directories.reverse() - else: - directories.sort(key=operator.attrgetter('name'), reverse=True) - - # Set correct owner, mtime and filemode on directories. - for tarinfo in directories: - dirpath = os.path.join(path, tarinfo.name) - try: - self.chown(tarinfo, dirpath) - self.utime(tarinfo, dirpath) - self.chmod(tarinfo, dirpath) - except ExtractError: - e = sys.exc_info()[1] - if self.errorlevel > 1: - raise - else: - self._dbg(1, "tarfile: %s" % e) - - -def main(argv, version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - tarball = download_setuptools() - _install(tarball) - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/doc/_ext/px_xlator.py b/doc/_ext/px_xlator.py index 98929e9c..6ad1063e 100644 --- a/doc/_ext/px_xlator.py +++ b/doc/_ext/px_xlator.py @@ -78,6 +78,7 @@ class PxTranslator(BaseHtmlXlator): def visit_desc_parameterlist(self, node):
self.body.append('(')
self.first_param = 1
+ self.param_separator = node.child_text_separator
def depart_desc_parameterlist(self, node):
self.body.append(')')
diff --git a/doc/api.rst b/doc/api.rst index 09b94af4..9ce1ee1e 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -8,6 +8,7 @@ Coverage API :history: 20090613T164000, final touches for 3.0 :history: 20100221T151500, docs for 3.3 (on the plane back from PyCon) :history: 20100725T211700, updated for 3.4. +:history: 20121111T235800, added a bit of clarification. The API to coverage.py is very simple, contained in a single module called @@ -20,11 +21,13 @@ in the command line interface. For example, a simple use would be:: cov = coverage.coverage() cov.start() - # .. run your code .. + # .. call your code .. cov.stop() cov.save() + cov.html_report() + The coverage module ------------------- diff --git a/doc/cmd.rst b/doc/cmd.rst index 45552089..6b1d15dc 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -16,13 +16,19 @@ Coverage command line usage :history: 20120504T091800, Added info about execution warnings, and 3.5.2 stuff. :history: 20120807T211600, Clarified the combine rules. :history: 20121003T074600, Fixed an option reference, https://bitbucket.org/ned/coveragepy/issue/200/documentation-mentions-output-xml-instead +:history: 20121117T091000, Added command aliases. .. highlight:: console When you install coverage.py, a command-line script simply called ``coverage`` -is placed in your Python scripts directory. Coverage has a number of commands -which determine the action performed: +is placed in your Python scripts directory. To help with multi-version +installs, it will also create either a ``coverage2`` or ``coverage3`` alias, +and a ``coverage-X.Y`` alias, depending on the version of Python you're using. +For example, when installing on Python 2.7, you will be able to use +``coverage``, ``coverage2``, or ``coverage-2.7`` on the command line. + +Coverage has a number of commands which determine the action performed: * **run** -- Run a Python program and collect execution data. @@ -204,6 +210,11 @@ encountered trying to find source files to report on. This can be useful if some files are missing, or if your Python execution is tricky enough that file names are synthesized without real source files. +If you provide a ``--fail-under`` value, the total percentage covered will be +compared to that value. If it is less, the command will exit with a status +code of 2, indicating that the total coverage was less than your target. This +can be used as part of a pass/fail condition, for example in a continuous +integration server. This option isn't available for **annotate**. .. _cmd_summary: @@ -269,13 +280,17 @@ Lines are highlighted green for executed, red for missing, and gray for excluded. The counts at the top of the file are buttons to turn on and off the highlighting. -If you prefer a different style for your HTML report, you can provide your -own CSS file to apply, by specifying a CSS file in the [html] section of the -configuration file. See :ref:`config_html` for details. - A number of keyboard shortcuts are available for navigating the report. Click the keyboard icon in the upper right to see the complete list. +The title of the report can be set with the ``title`` setting in the +``[html]`` section of the configuration file, or the ``--title`` switch on +the command line. + +If you prefer a different style for your HTML report, you can provide your +own CSS file to apply, by specifying a CSS file in the ``[html]`` section of +the configuration file. See :ref:`config_html` for details. + The ``-d`` argument specifies an output directory, defaulting to "htmlcov":: $ coverage html -d coverage_html diff --git a/doc/config.rst b/doc/config.rst index 74cab4fd..159a42f5 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -180,6 +180,9 @@ The file will be copied into the HTML output directory. Don't name it "style.css". This CSS is in addition to the CSS normally used, though you can overwrite as many of the rules as you like. +``title`` (string, default "Coverage report"): the title to use for the report. +Note this is text, not HTML. + [xml] ----- diff --git a/doc/contributing.rst b/doc/contributing.rst new file mode 100644 index 00000000..23eb1adb --- /dev/null +++ b/doc/contributing.rst @@ -0,0 +1,150 @@ +.. _contributing: + +=========================== +Contributing to coverage.py +=========================== + +:history: 20121112T154100, brand new docs. + +.. highlight:: console + +I welcome contributions to coverage.py. Over the years, dozens of people have +provided patches of various sizes to add features or fix bugs. This page +should have all the information you need to make a contribution. + +One source of history or ideas are the `bug reports`_ against coverage.py. +There you can find ideas for requested features, or the remains of rejected +ideas. + +.. _bug reports: https://bitbucket.org/ned/coveragepy/issues?status=new&status=open + + +Before you begin +---------------- + +If you have an idea for coverage.py, run it by me before you begin writing +code. This way, I can get you going in the right direction, or point you to +previous work in the area. Things are not always as straightforward as they +seem, and having the benefit of lessons learned by those before you can save +you frustration. + + +Getting the code +---------------- + +The coverage.py code is hosted on a `Mercurial`_ repository at +https://bitbucket.org/ned/coveragepy. To get a working environment, follow +these steps: + +#. (Optional, but recommended) Create a virtualenv to work in, and activate + it. + +#. Clone the repo:: + + $ hg clone https://bitbucket.org/ned/coveragepy + +#. Install the requirements:: + + $ pip install -r requirements.txt + +#. Install a number of versions of Python. Coverage.py supports a wide range + of Python versions. The more you can test with, the more easily your code + can be used as-is. If you only have one version, that's OK too, but may + mean more work integrating your contribution. + + +Running the tests +----------------- + +The tests are written as standard unittest-style tests, and are run with +`tox`_:: + + $ tox + GLOB sdist-make: /home/ned/coverage/setup.py + py25 sdist-reinst: /home/ned/coverage/tox/dist/coverage-3.6b1.zip + py25 runtests: commands[0] + py25 runtests: commands[1] + py25 runtests: commands[2] + py25 runtests: commands[3] + py25 runtests: commands[4] + === Python 2.5.5 with Python tracer (/home/ned/coverage/tox/py25/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 360 tests in 10.836s + + OK + py25 runtests: commands[5] + py25 runtests: commands[6] + === Python 2.5.5 with C tracer (/home/ned/coverage/tox/py25/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 360 tests in 10.044s + + OK + py26 sdist-reinst: /home/ned/coverage/trunk/.tox/dist/coverage-3.6b1.zip + py26 runtests: commands[0] + py26 runtests: commands[1] + py26 runtests: commands[2] + py26 runtests: commands[3] + py26 runtests: commands[4] + === CPython 2.6.6 with Python tracer (/home/ned/coverage/tox/py26/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 364 tests in 12.572s + + OK + py26 runtests: commands[5] + py26 runtests: commands[6] + === CPython 2.6.6 with C tracer (/home/ned/coverage/tox/py26/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 364 tests in 11.458s + + OK + (and so on...) + +Tox runs the complete test suite twice for each version of Python you have +installed. The first run uses the Python implementation of the trace +function, the second uses the C implementation. + +To limit tox to just a few versions of Python, use the ``-e`` switch:: + + $ tox -e py27,py33 + +To run just a few tests, you can use nose test selector syntax:: + + $ tox test.test_misc:SetupPyTest.test_metadata + +This looks in `test/test_misc.py` to find the `SetupPyTest` class, and runs the +`test_metadata` test method. + +Of course, run all the tests on every version of Python you have, before +submitting a change. + + +Lint, etc +--------- + +I try to keep the coverage.py as clean as possible. I use pylint to alert me +to possible problems:: + + $ make lint + pylint --rcfile=.pylintrc coverage setup.py test + python -m tabnanny coverage setup.py test + python igor.py check_eol + +The source is pylint-clean, even if it's because there are pragmas quieting +some warnings. Please try to keep it that way, but don't let pylint warnings +keep you from sending patches. I can clean them up. + + +Contributing +------------ + +When you are ready to contribute a change, any way you can get it to me is +probably fine. A pull request on Bitbucket is great, but a simple diff or +patch is great too. + + +.. _Mercurial: http://mercurial.selenic.com/ +.. _tox: http://tox.testrun.org/ diff --git a/doc/index.rst b/doc/index.rst index 9e8ed502..474e782f 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -24,6 +24,7 @@ coverage.py :history: 20120429T162100, updated for 3.5.2b1 :history: 20120503T233800, updated for 3.5.2 :history: 20120929T093500, updated for 3.5.3 +:history: 20121117T094900, Change from easy_install to pip. Coverage.py is a tool for measuring code coverage of Python programs. It @@ -55,7 +56,7 @@ Quick start Getting started is easy: #. Install coverage.py from the `coverage page on the Python Package Index`_, - or by using "easy_install coverage". For a few more details, see + or by using "pip install coverage". For a few more details, see :ref:`install`. #. Use ``coverage run`` to run your program and gather data: @@ -154,6 +155,7 @@ More information branch subprocess api + contributing faq changes diff --git a/doc/install.rst b/doc/install.rst index ffce72fa..91021b1b 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -13,13 +13,17 @@ Installation :history: 20120429T162500, updated for 3.5.2b1. :history: 20120503T234000, updated for 3.5.2. :history: 20120929T093600, updated for 3.5.3. +:history: 20121117T095000, Now setuptools is a pre-req. .. highlight:: console .. _coverage_pypi: http://pypi.python.org/pypi/coverage +.. _setuptools: http://pypi.python.org/pypi/setuptools +.. _Distribute: http://packages.python.org/distribute/ -Installing coverage.py is fairly standard: +Installing coverage.py is done in the usual ways. You must have `setuptools`_ +or `Distribute`_ installed already, and then you: #. Download the appropriate kit from the `coverage page on the Python Package Index`__. @@ -28,11 +32,11 @@ Installing coverage.py is fairly standard: or, use:: - $ easy_install coverage + $ pip install coverage or even:: - $ pip install coverage + $ easy_install coverage .. __: coverage_pypi_ @@ -55,7 +59,6 @@ installed as a pre-requisite, but otherwise are self-contained. They have the C extension pre-compiled so there's no need to worry about compilers. .. __: coverage_pypi_ -.. _setuptools: http://pypi.python.org/pypi/setuptools Checking the installation diff --git a/ez_setup.py b/ez_setup.py deleted file mode 100644 index d24e845e..00000000 --- a/ez_setup.py +++ /dev/null @@ -1,276 +0,0 @@ -#!python -"""Bootstrap setuptools installation - -If you want to use setuptools in your package's setup.py, just include this -file in the same directory with it, and add this to the top of your setup.py:: - - from ez_setup import use_setuptools - use_setuptools() - -If you want to require a specific version of setuptools, set a download -mirror, or use an alternate download directory, you can do so by supplying -the appropriate options to ``use_setuptools()``. - -This file can also be run as a script to install or upgrade setuptools. -""" -import sys -DEFAULT_VERSION = "0.6c9" -DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] - -md5_data = { - 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', - 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', - 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', - 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', - 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', - 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', - 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', - 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', - 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', - 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', - 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', - 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', - 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', - 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', - 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', - 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', - 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', - 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', - 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', - 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', - 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', - 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', - 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', - 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', - 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', - 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', - 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', - 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', - 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', - 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', - 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', - 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', - 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', - 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', -} - -import sys, os -try: from hashlib import md5 -except ImportError: from md5 import md5 - -def _validate_md5(egg_name, data): - if egg_name in md5_data: - digest = md5(data).hexdigest() - if digest != md5_data[egg_name]: - print >>sys.stderr, ( - "md5 validation of %s failed! (Possible download problem?)" - % egg_name - ) - sys.exit(2) - return data - -def use_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, - download_delay=15 -): - """Automatically find/download setuptools and make it available on sys.path - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end with - a '/'). `to_dir` is the directory where setuptools will be downloaded, if - it is not already available. If `download_delay` is specified, it should - be the number of seconds that will be paused before initiating a download, - should one be required. If an older version of setuptools is installed, - this routine will print a message to ``sys.stderr`` and raise SystemExit in - an attempt to abort the calling script. - """ - was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules - def do_download(): - egg = download_setuptools(version, download_base, to_dir, download_delay) - sys.path.insert(0, egg) - import setuptools; setuptools.bootstrap_install_from = egg - try: - import pkg_resources - except ImportError: - return do_download() - try: - pkg_resources.require("setuptools>="+version); return - except pkg_resources.VersionConflict, e: - if was_imported: - print >>sys.stderr, ( - "The required version of setuptools (>=%s) is not available, and\n" - "can't be installed while this script is running. Please install\n" - " a more recent version first, using 'easy_install -U setuptools'." - "\n\n(Currently using %r)" - ) % (version, e.args[0]) - sys.exit(2) - else: - del pkg_resources, sys.modules['pkg_resources'] # reload ok - return do_download() - except pkg_resources.DistributionNotFound: - return do_download() - -def download_setuptools( - version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, - delay = 15 -): - """Download setuptools from a specified location and return its filename - - `version` should be a valid setuptools version number that is available - as an egg for download under the `download_base` URL (which should end - with a '/'). `to_dir` is the directory where the egg will be downloaded. - `delay` is the number of seconds to pause before an actual download attempt. - """ - import urllib2, shutil - egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) - url = download_base + egg_name - saveto = os.path.join(to_dir, egg_name) - src = dst = None - if not os.path.exists(saveto): # Avoid repeated downloads - try: - from distutils import log - if delay: - log.warn(""" ---------------------------------------------------------------------------- -This script requires setuptools version %s to run (even to display -help). I will attempt to download it for you (from -%s), but -you may need to enable firewall access for this script first. -I will start the download in %d seconds. - -(Note: if this machine does not have network access, please obtain the file - - %s - -and place it in this directory before rerunning this script.) ----------------------------------------------------------------------------""", - version, download_base, delay, url - ); from time import sleep; sleep(delay) - log.warn("Downloading %s", url) - src = urllib2.urlopen(url) - # Read/write all in one block, so we don't create a corrupt file - # if the download is interrupted. - data = _validate_md5(egg_name, src.read()) - dst = open(saveto,"wb"); dst.write(data) - finally: - if src: src.close() - if dst: dst.close() - return os.path.realpath(saveto) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -def main(argv, version=DEFAULT_VERSION): - """Install or upgrade setuptools and EasyInstall""" - try: - import setuptools - except ImportError: - egg = None - try: - egg = download_setuptools(version, delay=0) - sys.path.insert(0,egg) - from setuptools.command.easy_install import main - return main(list(argv)+[egg]) # we're done here - finally: - if egg and os.path.exists(egg): - os.unlink(egg) - else: - if setuptools.__version__ == '0.0.1': - print >>sys.stderr, ( - "You have an obsolete version of setuptools installed. Please\n" - "remove it from your system entirely before rerunning this script." - ) - sys.exit(2) - - req = "setuptools>="+version - import pkg_resources - try: - pkg_resources.require(req) - except pkg_resources.VersionConflict: - try: - from setuptools.command.easy_install import main - except ImportError: - from easy_install import main - main(list(argv)+[download_setuptools(delay=0)]) - sys.exit(0) # try to force an exit - else: - if argv: - from setuptools.command.easy_install import main - main(argv) - else: - print "Setuptools version",version,"or greater has been installed." - print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' - -def update_md5(filenames): - """Update our built-in md5 registry""" - - import re - - for name in filenames: - base = os.path.basename(name) - f = open(name,'rb') - md5_data[base] = md5(f.read()).hexdigest() - f.close() - - data = [" %r: %r,\n" % it for it in md5_data.items()] - data.sort() - repl = "".join(data) - - import inspect - srcfile = inspect.getsourcefile(sys.modules[__name__]) - f = open(srcfile, 'rb'); src = f.read(); f.close() - - match = re.search("\nmd5_data = {\n([^}]+)}", src) - if not match: - print >>sys.stderr, "Internal error!" - sys.exit(2) - - src = src[:match.start(1)] + repl + src[match.end(1):] - f = open(srcfile,'w') - f.write(src) - f.close() - - -if __name__=='__main__': - if len(sys.argv)>2 and sys.argv[1]=='--md5update': - update_md5(sys.argv[2:]) - else: - main(sys.argv[1:]) - - - - - - @@ -5,7 +5,7 @@ - Ubuntu - Mac - Pythons 2.3, 2.4, 2.5, 2.6, 2.7, 3.1, 3.2, 3.3 -- Version number in coverage/__init__.py +- Version number in coverage/version.py - 3.1a1, 3.1b1, 3.1c1, 3.1 - Update CHANGES.txt, including release date. - Update docstring in setup.py, including "New in x.y:" @@ -55,7 +55,10 @@ def do_zip_mods(args): def do_check_eol(args): """Check files for incorrect newlines and trailing whitespace.""" - ignore_dirs = ['.svn', '.hg', '.tox', '.tox_kits'] + ignore_dirs = [ + '.svn', '.hg', '.tox', '.tox_kits', 'coverage.egg-info', + '_build', + ] checked = set([]) def check_file(fname, crlf=True, trail_white=True): diff --git a/doc/branches.py b/lab/branches.py index 1fa705f0..1fa705f0 100644 --- a/doc/branches.py +++ b/lab/branches.py diff --git a/lab/cmd-opts.txt b/lab/cmd-opts.txt deleted file mode 100644 index 3397d641..00000000 --- a/lab/cmd-opts.txt +++ /dev/null @@ -1,78 +0,0 @@ -coverage -e -x foo.py -coverage run foo.py -coverage run --timid foo.py -coverage foo.py * - - -global args: - - -h --help - --rcfile string - --version - - -commands: - - annotate -a - -d --output-directory string - -i --ignore-errors bool [report].ignore_errors - --omit list of string [report].omit - - combine -c - - debug - - erase -e - - help - - html -b - -d --output-directory string - -i --ignore-errors bool [report].ignore_errors - --omit list of string [report].omit - --red * - --green * - --style * - - report -r - -m --missing bool - -i --ignore-errors bool [report].ignore_errors - --omit list of string [report].omit - - run -x - -a --append bool - --branch bool [run].branch - -L --pylib bool [run].cover_pylib - -p --parallel bool - --timid bool [run].timid - - - --include=directory * [run].include - --include=filename * - --include=module * - --exclude=directory * [run].exclude - - - - xml - -i --ignore-errors bool [report].ignore_errors - --omit list of string [report].omit - -o string - - - - byteprep * - - -Other config: - - [report].exclude_lines list of string - [run].data_file string - - -Option scopes: - - - Cmd line - - coverage() - - ini file - - environment variable diff --git a/metacov.ini b/metacov.ini index 7d009ea9..9d8b4d97 100644 --- a/metacov.ini +++ b/metacov.ini @@ -15,5 +15,5 @@ exclude_lines = if __name__ == .__main__.:
raise AssertionError
-omit = mock.py, ez_setup.py, distribute.py
+omit = mock.py
ignore_errors = true
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..4c667742 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +nose +mock +tox +pylint +sphinx @@ -1,4 +1,4 @@ -# setup.py for coverage. +# setup.py for coverage.py """Code coverage measurement for Python @@ -19,12 +19,11 @@ can be reported. New in 3.3: .coveragerc files. New in 3.2: Branch coverage! - """ # This file is used unchanged under all versions of Python, 2.x and 3.x. -classifiers = """ +classifiers = """\ Environment :: Console Intended Audience :: Developers License :: OSI Approved :: BSD License @@ -36,28 +35,26 @@ Topic :: Software Development :: Testing """ # Pull in the tools we need. -import sys - -# Distribute is a new fork of setuptools. It's supported on Py3.x, so we use -# it there, but stick with classic setuptools on Py2.x until Distribute becomes -# more accepted. -if sys.version_info >= (3, 0): - from distribute_setup import use_setuptools -else: - from ez_setup import use_setuptools - -use_setuptools() +import os, sys from setuptools import setup from distutils.core import Extension # pylint: disable=E0611,F0401 -# Get or massage our metadata. +# Get or massage our metadata. We exec coverage/version.py so we can avoid +# importing the product code into setup.py. -from coverage import __url__, __version__ +doc = __doc__ # __doc__ will be overwritten by version.py. +__version__ = __url__ = "" # keep pylint happy. -doclines = (__doc__ % __url__).split('\n') +cov_ver_py = os.path.join(os.path.split(__file__)[0], "coverage/version.py") +version_file = open(cov_ver_py) +try: + exec(compile(version_file.read(), cov_ver_py, 'exec')) +finally: + version_file.close() -classifier_list = [c for c in classifiers.split("\n") if c] +doclines = (doc % __url__).splitlines() +classifier_list = classifiers.splitlines() if 'a' in __version__: devstat = "3 - Alpha" @@ -67,7 +64,15 @@ else: devstat = "5 - Production/Stable" classifier_list.append("Development Status :: " + devstat) -# Set it up! +# Install a script as "coverage", and as "coverage[23]", and as +# "coverage-2.7" (or whatever). +scripts = [ + 'coverage = coverage:main', + 'coverage%d = coverage:main' % sys.version_info[:1], + 'coverage-%d.%d = coverage:main' % sys.version_info[:2], + ] + +# Create the keyword arguments for setup() setup_args = dict( name = 'coverage', @@ -83,11 +88,7 @@ setup_args = dict( ] }, - entry_points = { - 'console_scripts': [ - 'coverage = coverage:main', - ], - }, + entry_points = {'console_scripts': scripts}, # We need to get HTML assets from our htmlfiles dir. zip_safe = False, @@ -126,20 +127,25 @@ if sys.version_info >= (3, 0): use_2to3=False, )) -# For a variety of reasons, it might not be possible to install the C -# extension. Try it with, and if it fails, try it without. -try: - setup(**setup_args) -except: # pylint: disable=W0702 - # When setup() can't compile, it tries to exit. We'll catch SystemExit - # here :-(, and try again. - if 'install' not in sys.argv or 'ext_modules' not in setup_args: - # We weren't trying to install an extension, so forget it. - raise - msg = "Couldn't install with extension module, trying without it..." - exc = sys.exc_info()[1] - exc_msg = "%s: %s" % (exc.__class__.__name__, exc) - print("**\n** %s\n** %s\n**" % (msg, exc_msg)) - - del setup_args['ext_modules'] - setup(**setup_args) +def main(): + """Actually invoke setup() with the arguments we built above.""" + # For a variety of reasons, it might not be possible to install the C + # extension. Try it with, and if it fails, try it without. + try: + setup(**setup_args) + except: # pylint: disable=W0702 + # When setup() can't compile, it tries to exit. We'll catch SystemExit + # here :-(, and try again. + if 'install' not in sys.argv or 'ext_modules' not in setup_args: + # We weren't trying to install an extension, so forget it. + raise + msg = "Couldn't install with extension module, trying without it..." + exc = sys.exc_info()[1] + exc_msg = "%s: %s" % (exc.__class__.__name__, exc) + print("**\n** %s\n** %s\n**" % (msg, exc_msg)) + + del setup_args['ext_modules'] + setup(**setup_args) + +if __name__ == '__main__': + main() diff --git a/switchpy.cmd b/switchpy.cmd deleted file mode 100644 index 089df2a5..00000000 --- a/switchpy.cmd +++ /dev/null @@ -1,14 +0,0 @@ -@echo off
-set PATH=.;%HOME%\bin
-set PATH=%PATH%;c:\windows\system32;c:\windows
-set PATH=%PATH%;%1\scripts;%1
-set PATH=%PATH%;C:\Program Files\Mercurial\bin
-set PATH=%PATH%;c:\cygwin\bin
-set PATH=%PATH%;c:\app\MinGW\bin
-set PYTHONPATH=;c:\ned\py
-set PYHOME=%1
-rem title Py %1
-if "%2"=="quiet" goto done
-python -c "import sys; print ('\n=== Python %%s %%s' %% (sys.version.split()[0], '='*80))"
-
-:done
diff --git a/test/backtest.py b/test/backtest.py index c54171d3..b17aa242 100644 --- a/test/backtest.py +++ b/test/backtest.py @@ -31,7 +31,7 @@ else: stderr=subprocess.STDOUT ) output, _ = proc.communicate() - status = proc.returncode + status = proc.returncode # pylint: disable=E1101 # Get the output, and canonicalize it to strings with newlines. if not isinstance(output, str): diff --git a/test/backunittest.py b/test/backunittest.py index c1685e0c..30da78eb 100644 --- a/test/backunittest.py +++ b/test/backunittest.py @@ -29,6 +29,27 @@ class TestCase(unittest.TestCase): if exp: self.fail(msg) + if _need('assertIn'): + def assertIn(self, member, container, msg=None): + """Assert that `member` is in `container`.""" + if member not in container: + msg = msg or ('%r not found in %r' % (member, container)) + self.fail(msg) + + if _need('assertNotIn'): + def assertNotIn(self, member, container, msg=None): + """Assert that `member` is not in `container`.""" + if member in container: + msg = msg or ('%r found in %r' % (member, container)) + self.fail(msg) + + if _need('assertGreater'): + def assertGreater(self, a, b, msg=None): + """Assert that `a` is greater than `b`.""" + if not a > b: + msg = msg or ('%r not greater than %r' % (a, b)) + self.fail(msg) + if _need('assertRaisesRegexp'): def assertRaisesRegexp(self, excClass, regexp, callobj, *args, **kw): """ Just like unittest.TestCase.assertRaises, @@ -46,7 +67,7 @@ class TestCase(unittest.TestCase): # Message provided, and it didn't match: fail! raise self.failureException( "Right exception, wrong message: " - "'%s' doesn't match '%s'" % (excMsg, regexp) + "%r doesn't match %r" % (excMsg, regexp) ) # No need to catch other exceptions: They'll fail the test all by # themselves! @@ -66,11 +87,12 @@ class TestCase(unittest.TestCase): self.assertEqual(set(s1), set(s2)) if _need('assertRegexpMatches'): - def assertRegexpMatches(self, s, regex): - """Assert that `s` matches `regex`.""" - m = re.search(regex, s) + def assertRegexpMatches(self, text, regex, msg=None): + """Assert that `text` matches `regex`.""" + m = re.search(regex, text) if not m: - raise self.failureException("%r doesn't match %r" % (s, regex)) + msg = msg or ("%r doesn't match %r" % (text, regex)) + raise self.failureException(msg) if _need('assertMultiLineEqual'): def assertMultiLineEqual(self, first, second, msg=None): diff --git a/test/test_api.py b/test/test_api.py index aa0e726b..c15db5ed 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -84,7 +84,7 @@ class SingletonApiTest(CoverageTest): self.do_report_work("mycode4") coverage.report() rpt = re.sub(r"\s+", " ", self.stdout()) - self.assertTrue("mycode4 7 3 57% 4-6" in rpt) + self.assertIn("mycode4 7 3 57% 4-6", rpt) class ApiTest(CoverageTest): @@ -366,16 +366,12 @@ class OmitIncludeTestsMixin(UsingModulesMixin): def filenames_in(self, summary, filenames): """Assert the `filenames` are in the keys of `summary`.""" for filename in filenames.split(): - self.assert_(filename in summary, - "%s should be in %r" % (filename, summary) - ) + self.assertIn(filename, summary) def filenames_not_in(self, summary, filenames): """Assert the `filenames` are not in the keys of `summary`.""" for filename in filenames.split(): - self.assert_(filename not in summary, - "%s should not be in %r" % (filename, summary) - ) + self.assertNotIn(filename, summary) def test_nothing_specified(self): result = self.coverage_usepkgs() @@ -433,6 +429,7 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): cov.start() import usepkgs # pylint: disable=F0401,W0612 cov.stop() + cov._harvest_data() # private! sshhh... summary = cov.data.summary() for k, v in list(summary.items()): assert k.endswith(".py") @@ -514,4 +511,5 @@ class AnalysisTest(CoverageTest): self.assertEqual(nums.n_excluded, 1) self.assertEqual(nums.n_missing, 3) self.assertEqual(nums.n_branches, 2) - self.assertEqual(nums.n_missing_branches, 0) + self.assertEqual(nums.n_partial_branches, 0) + self.assertEqual(nums.n_missing_branches, 2) diff --git a/test/test_arcs.py b/test/test_arcs.py index ce550042..a9f7470b 100644 --- a/test/test_arcs.py +++ b/test/test_arcs.py @@ -450,18 +450,19 @@ class ExceptionArcTest(CoverageTest): arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB AD BC CD D.", arcz_missing="3D AB BC CD", arcz_unpredicted="") - def xxx_xest_finally_in_loop_2(self): - self.check_coverage("""\ - for i in range(5): - try: - j = 3 - finally: - f = 5 - g = 6 - h = 7 - """, - arcz=".1 12 23 35 56 61 17 7.", - arcz_missing="", arcz_unpredicted="") + if 0: + def test_finally_in_loop_2(self): + self.check_coverage("""\ + for i in range(5): + try: + j = 3 + finally: + f = 5 + g = 6 + h = 7 + """, + arcz=".1 12 23 35 56 61 17 7.", + arcz_missing="", arcz_unpredicted="") if sys.version_info >= (2, 5): # Try-except-finally was new in 2.5 diff --git a/test/test_cmdline.py b/test/test_cmdline.py index d4cc763d..eb7fe0f5 100644 --- a/test/test_cmdline.py +++ b/test/test_cmdline.py @@ -251,35 +251,35 @@ class ClassicCmdLineTest(CmdLineTest): def test_html_report(self): # coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] self.cmd_executes("-b", self.INIT_LOAD + """\ - .html_report(directory=None, ignore_errors=None, + .html_report(directory=None, ignore_errors=None, title=None, omit=None, include=None, morfs=[]) """) self.cmd_executes("-b -d dir1", self.INIT_LOAD + """\ - .html_report(directory="dir1", ignore_errors=None, + .html_report(directory="dir1", ignore_errors=None, title=None, omit=None, include=None, morfs=[]) """) self.cmd_executes("-b -i", self.INIT_LOAD + """\ - .html_report(directory=None, ignore_errors=True, + .html_report(directory=None, ignore_errors=True, title=None, omit=None, include=None, morfs=[]) """) self.cmd_executes("-b -o fooey", """\ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"]) .load() - .html_report(directory=None, ignore_errors=None, + .html_report(directory=None, ignore_errors=None, title=None, omit=["fooey"], include=None, morfs=[]) """) self.cmd_executes("-b -o fooey,booey", """\ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"]) .load() - .html_report(directory=None, ignore_errors=None, + .html_report(directory=None, ignore_errors=None, title=None, omit=["fooey", "booey"], include=None, morfs=[]) """) self.cmd_executes("-b mod1", self.INIT_LOAD + """\ - .html_report(directory=None, ignore_errors=None, + .html_report(directory=None, ignore_errors=None, title=None, omit=None, include=None, morfs=["mod1"]) """) self.cmd_executes("-b mod1 mod2 mod3", self.INIT_LOAD + """\ - .html_report(directory=None, ignore_errors=None, + .html_report(directory=None, ignore_errors=None, title=None, omit=None, include=None, morfs=["mod1", "mod2", "mod3"]) """) @@ -447,6 +447,14 @@ class NewCmdLineTest(CmdLineTest): self.cmd_executes_same("html --omit f,b", "-b --omit f,b") self.cmd_executes_same("html m1", "-b m1") self.cmd_executes_same("html m1 m2 m3", "-b m1 m2 m3") + self.cmd_executes("html", self.INIT_LOAD + """\ + .html_report(ignore_errors=None, omit=None, include=None, morfs=[], + directory=None, title=None) + """) + self.cmd_executes("html --title=Hello_there", self.INIT_LOAD + """\ + .html_report(ignore_errors=None, omit=None, include=None, morfs=[], + directory=None, title='Hello_there') + """) def test_report(self): self.cmd_executes_same("report", "-r") @@ -553,11 +561,11 @@ class NewCmdLineTest(CmdLineTest): # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("xml", self.INIT_LOAD + """\ .xml_report(ignore_errors=None, omit=None, include=None, morfs=[], - outfile="coverage.xml") + outfile=None) """) self.cmd_executes("xml -i", self.INIT_LOAD + """\ .xml_report(ignore_errors=True, omit=None, include=None, morfs=[], - outfile="coverage.xml") + outfile=None) """) self.cmd_executes("xml -o myxml.foo", self.INIT_LOAD + """\ .xml_report(ignore_errors=None, omit=None, include=None, morfs=[], @@ -571,21 +579,21 @@ class NewCmdLineTest(CmdLineTest): .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"]) .load() .xml_report(ignore_errors=None, omit=["fooey"], include=None, morfs=[], - outfile="coverage.xml") + outfile=None) """) self.cmd_executes("xml --omit fooey,booey", """\ .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"]) .load() .xml_report(ignore_errors=None, omit=["fooey", "booey"], include=None, - morfs=[], outfile="coverage.xml") + morfs=[], outfile=None) """) self.cmd_executes("xml mod1", self.INIT_LOAD + """\ .xml_report(ignore_errors=None, omit=None, include=None, morfs=["mod1"], - outfile="coverage.xml") + outfile=None) """) self.cmd_executes("xml mod1 mod2 mod3", self.INIT_LOAD + """\ .xml_report(ignore_errors=None, omit=None, include=None, - morfs=["mod1", "mod2", "mod3"], outfile="coverage.xml") + morfs=["mod1", "mod2", "mod3"], outfile=None) """) def test_no_arguments_at_all(self): @@ -604,6 +612,12 @@ class CmdLineStdoutTest(CmdLineTest): assert "Code coverage for Python." in out assert out.count("\n") < 4 + def test_version(self): + self.command_line("--version") + out = self.stdout() + assert "ersion " in out + assert out.count("\n") < 4 + def test_help(self): self.command_line("help") out = self.stdout() diff --git a/test/test_config.py b/test/test_config.py index 4fc658f4..19e37ab9 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Test the config file handling for coverage.py""" import os, sys @@ -139,6 +140,7 @@ class ConfigFileTest(CoverageTest): directory = c:\\tricky\\dir.somewhere extra_css=something/extra.css + title = Title & nums # nums! [xml] output=mycov.xml @@ -159,7 +161,7 @@ class ConfigFileTest(CoverageTest): self.assertTrue(cov.config.parallel) self.assertEqual(cov.get_exclude_list(), - ["if 0:", "pragma:?\s+no cover", "another_tab"] + ["if 0:", r"pragma:?\s+no cover", "another_tab"] ) self.assertTrue(cov.config.ignore_errors) self.assertEqual(cov.config.include, ["a/", "b/"]) @@ -169,7 +171,7 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.precision, 3) self.assertEqual(cov.config.partial_list, - ["pragma:?\s+no branch"] + [r"pragma:?\s+no branch"] ) self.assertEqual(cov.config.partial_always_list, ["if 0:", "while True:"] @@ -177,6 +179,7 @@ class ConfigFileTest(CoverageTest): self.assertTrue(cov.config.show_missing) self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere") self.assertEqual(cov.config.extra_css, "something/extra.css") + self.assertEqual(cov.config.html_title, "Title & nums # nums!") self.assertEqual(cov.config.xml_output, "mycov.xml") @@ -184,3 +187,16 @@ class ConfigFileTest(CoverageTest): 'source': ['.', '/home/ned/src/'], 'other': ['other', '/home/ned/other', 'c:\\Ned\\etc'] }) + + if sys.version_info[:2] != (3,1): + def test_one(self): + # This sample file tries to use lots of variation of syntax... + self.make_file(".coveragerc", """\ + [html] + title = tabblo & «ταБЬℓσ» # numbers + """) + cov = coverage.coverage() + + self.assertEqual(cov.config.html_title, + "tabblo & «ταБЬℓσ» # numbers" + ) diff --git a/test/test_files.py b/test/test_files.py index 207274a2..5692699c 100644 --- a/test/test_files.py +++ b/test/test_files.py @@ -38,7 +38,7 @@ class FileLocatorTest(CoverageTest): self.assertEqual(fl.relative_filename(a2), a2) def test_filepath_contains_absolute_prefix_twice(self): - # https://bitbucket.org/ned/coveragepy/issue/194/filelocatorrelative_filename-could-mangle + # https://bitbucket.org/ned/coveragepy/issue/194 # Build a path that has two pieces matching the absolute path prefix. # Technically, this test doesn't do that on Windows, but drive # letters make that impractical to acheive. diff --git a/test/test_html.py b/test/test_html.py index 1877a30d..d33dd1f3 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Tests that HTML generation is awesome.""" import os.path, sys @@ -7,19 +8,8 @@ from coverage.misc import NotPython sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k from coveragetest import CoverageTest -class HtmlTest(CoverageTest): - """HTML!""" - - def setUp(self): - super(HtmlTest, self).setUp() - - # At least one of our tests monkey-patches the version of coverage, - # so grab it here to restore it later. - self.real_coverage_version = coverage.__version__ - - def tearDown(self): - coverage.__version__ = self.real_coverage_version - super(HtmlTest, self).tearDown() +class HtmlTestHelpers(CoverageTest): + """Methods that help with HTML tests.""" def create_initial_files(self): """Create the source files we need to run these tests.""" @@ -38,14 +28,14 @@ class HtmlTest(CoverageTest): print("x is %d" % x) """) - def run_coverage(self, **kwargs): + def run_coverage(self, covargs=None, htmlargs=None): """Run coverage on main_file.py, and create an HTML report.""" self.clean_local_file_imports() - cov = coverage.coverage(**kwargs) + cov = coverage.coverage(**(covargs or {})) cov.start() self.import_local_file("main_file") cov.stop() - cov.html_report() + cov.html_report(**(htmlargs or {})) def remove_html_files(self): """Remove the HTML files created as part of the HTML report.""" @@ -54,6 +44,23 @@ class HtmlTest(CoverageTest): os.remove("htmlcov/helper1.html") os.remove("htmlcov/helper2.html") + +class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): + """Tests of the HTML delta speed-ups.""" + + def setUp(self): + super(HtmlDeltaTest, self).setUp() + + # At least one of our tests monkey-patches the version of coverage, + # so grab it here to restore it later. + self.real_coverage_version = coverage.__version__ + + self.maxDiff = None + + def tearDown(self): + coverage.__version__ = self.real_coverage_version + super(HtmlDeltaTest, self).tearDown() + def test_html_created(self): # Test basic HTML generation: files should be created. self.create_initial_files() @@ -118,11 +125,11 @@ class HtmlTest(CoverageTest): # In this case, everything changes because the coverage settings have # changed. self.create_initial_files() - self.run_coverage(timid=False) + self.run_coverage(covargs=dict(timid=False)) index1 = open("htmlcov/index.html").read() self.remove_html_files() - self.run_coverage(timid=True) + self.run_coverage(covargs=dict(timid=True)) # All the files have been reported again. self.assert_exists("htmlcov/index.html") @@ -156,6 +163,56 @@ class HtmlTest(CoverageTest): self.assertMultiLineEqual(index1, fixed_index2) +class HtmlTitleTests(HtmlTestHelpers, CoverageTest): + """Tests of the HTML title support.""" + + def test_default_title(self): + self.create_initial_files() + self.run_coverage() + index = open("htmlcov/index.html").read() + self.assertIn("<title>Coverage report</title>", index) + self.assertIn("<h1>Coverage report:", index) + + def test_title_set_in_config_file(self): + self.create_initial_files() + self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n") + self.run_coverage() + index = open("htmlcov/index.html").read() + self.assertIn("<title>Metrics & stuff!</title>", index) + self.assertIn("<h1>Metrics & stuff!:", index) + + if sys.version_info[:2] != (3,1): + def test_non_ascii_title_set_in_config_file(self): + self.create_initial_files() + self.make_file(".coveragerc", + "[html]\ntitle = «ταБЬℓσ» numbers" + ) + self.run_coverage() + index = open("htmlcov/index.html").read() + self.assertIn( + "<title>«ταБЬℓσ»" + " numbers", index + ) + self.assertIn( + "<h1>«ταБЬℓσ»" + " numbers", index + ) + + def test_title_set_in_args(self): + self.create_initial_files() + self.make_file(".coveragerc", "[html]\ntitle = Good title\n") + self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!")) + index = open("htmlcov/index.html").read() + self.assertIn( + "<title>«ταБЬℓσ»" + " & stüff!</title>", index + ) + self.assertIn( + "<h1>«ταБЬℓσ»" + " & stüff!:", index + ) + + class HtmlWithUnparsableFilesTest(CoverageTest): """Test the behavior when measuring unparsable files.""" @@ -197,13 +254,9 @@ class HtmlWithUnparsableFilesTest(CoverageTest): self.assertEqual(output.strip(), "No data to report.") def test_execed_liar_ignored(self): - """ - Jinja2 sets __file__ to be a non-Python file, and then execs code. - - If that file contains non-Python code, a TokenError shouldn't - have been raised when writing the HTML report. - - """ + # Jinja2 sets __file__ to be a non-Python file, and then execs code. + # If that file contains non-Python code, a TokenError shouldn't + # have been raised when writing the HTML report. if sys.version_info < (3, 0): source = "exec compile('','','exec') in {'__file__': 'liar.html'}" else: diff --git a/test/test_misc.py b/test/test_misc.py index eb73cc81..ac53cddb 100644 --- a/test/test_misc.py +++ b/test/test_misc.py @@ -2,7 +2,8 @@ import os, sys -from coverage.misc import Hasher +from coverage.misc import Hasher, file_be_gone +from coverage import __version__, __url__ sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k from coveragetest import CoverageTest @@ -25,3 +26,51 @@ class HasherTest(CoverageTest): h2 = Hasher() h2.update({'b': 23, 'a': 17}) self.assertEqual(h1.digest(), h2.digest()) + + +class RemoveFileTest(CoverageTest): + """Tests of misc.file_be_gone.""" + + def test_remove_nonexistent_file(self): + # it's ok to try to remove a file that doesn't exist. + file_be_gone("not_here.txt") + + def test_remove_actual_file(self): + # it really does remove a file that does exist. + self.make_file("here.txt", "We are here, we are here, we are here!") + file_be_gone("here.txt") + self.assert_doesnt_exist("here.txt") + + def test_actual_errors(self): + # Errors can still happen. + # ". is a directory" on Unix, or "Access denied" on Windows + self.assertRaises(OSError, file_be_gone, ".") + + +class SetupPyTest(CoverageTest): + """Tests of setup.py""" + + run_in_temp_dir = False + + def test_metadata(self): + status, output = self.run_command_status( + "python setup.py --description --version --url --author" + ) + self.assertEqual(status, 0) + out = output.splitlines() + self.assertIn("measurement", out[0]) + self.assertEqual(out[1], __version__) + self.assertEqual(out[2], __url__) + self.assertIn("Ned Batchelder", out[3]) + + def test_more_metadata(self): + from setup import setup_args + + classifiers = setup_args['classifiers'] + self.assertGreater(len(classifiers), 7) + self.assertTrue(classifiers[-1].startswith("Development Status ::")) + + long_description = setup_args['long_description'].splitlines() + self.assertGreater(len(long_description), 7) + self.assertNotEqual(long_description[0].strip(), "") + self.assertNotEqual(long_description[-1].strip(), "") diff --git a/test/test_oddball.py b/test/test_oddball.py index 1a3bd22f..a8c243de 100644 --- a/test/test_oddball.py +++ b/test/test_oddball.py @@ -307,6 +307,7 @@ class ExceptionTest(CoverageTest): # Clean the line data and compare to expected results. # The filenames are absolute, so keep just the base. + cov._harvest_data() # private! sshhh... lines = cov.data.line_data() clean_lines = {} for f, llist in lines.items(): diff --git a/test/test_phystokens.py b/test/test_phystokens.py index 0e778510..d4e417e8 100644 --- a/test/test_phystokens.py +++ b/test/test_phystokens.py @@ -37,8 +37,8 @@ class PhysTokensTest(CoverageTest): # source_token_lines doesn't preserve trailing spaces, so trim all that # before comparing. source = source.replace('\r\n', '\n') - source = re.sub("(?m)[ \t]+$", "", source) - tokenized = re.sub("(?m)[ \t]+$", "", tokenized) + source = re.sub(r"(?m)[ \t]+$", "", source) + tokenized = re.sub(r"(?m)[ \t]+$", "", tokenized) self.assertMultiLineEqual(source, tokenized) def check_file_tokenization(self, fname): diff --git a/test/test_process.py b/test/test_process.py index f8d1b8d1..2d926038 100644 --- a/test/test_process.py +++ b/test/test_process.py @@ -175,8 +175,8 @@ class ProcessTest(CoverageTest): data.read_file(".coverage") summary = data.summary(fullpath=True) self.assertEqual(len(summary), 1) - actual = os.path.abspath(list(summary.keys())[0]) - expected = os.path.abspath('src/x.py') + actual = os.path.normcase(os.path.abspath(list(summary.keys())[0])) + expected = os.path.normcase(os.path.abspath('src/x.py')) self.assertEqual(actual, expected) self.assertEqual(list(summary.values())[0], 6) @@ -190,7 +190,7 @@ class ProcessTest(CoverageTest): os.remove("fleeting.py") out = self.run_command("coverage html -d htmlcov") self.assertRegexpMatches(out, "No source for code: '.*fleeting.py'") - self.assertFalse("Traceback" in out) + self.assertNotIn("Traceback", out) # It happens that the code paths are different for *.py and other # files, so try again with no extension. @@ -202,13 +202,13 @@ class ProcessTest(CoverageTest): os.remove("fleeting") status, out = self.run_command_status("coverage html -d htmlcov", 1) self.assertRegexpMatches(out, "No source for code: '.*fleeting'") - self.assertFalse("Traceback" in out) + self.assertNotIn("Traceback", out) self.assertEqual(status, 1) def test_running_missing_file(self): status, out = self.run_command_status("coverage run xyzzy.py", 1) self.assertRegexpMatches(out, "No file to run: .*xyzzy.py") - self.assertFalse("Traceback" in out) + self.assertNotIn("Traceback", out) self.assertEqual(status, 1) def test_code_throws(self): @@ -233,9 +233,9 @@ class ProcessTest(CoverageTest): self.assertMultiLineEqual(out, out2) # But also make sure that the output is what we expect. - self.assertTrue('File "throw.py", line 5, in f2' in out) - self.assertTrue('raise Exception("hey!")' in out) - self.assertFalse('coverage' in out) + self.assertIn('File "throw.py", line 5, in f2', out) + self.assertIn('raise Exception("hey!")', out) + self.assertNotIn('coverage', out) self.assertEqual(status, 1) def test_code_exits(self): @@ -290,6 +290,15 @@ class ProcessTest(CoverageTest): out2 = self.run_command("python -m test.try_execfile") self.assertMultiLineEqual(out, out2) + if 0: + # For https://bitbucket.org/ned/coveragepy/issue/207 + def test_coverage_run_dashm_is_like_python_dashm_with__main__(self): + self.make_file("package/__init__.py") # empty + self.make_file("package/__main__.py", "#\n") # empty + out = self.run_command("coverage run -m package") + out2 = self.run_command("python -m package") + self.assertMultiLineEqual(out, out2) + if hasattr(os, 'fork'): def test_fork(self): self.make_file("fork.py", """\ @@ -337,25 +346,27 @@ class ProcessTest(CoverageTest): """) out = self.run_command("coverage run --source=sys,xyzzy,quux hello.py") - self.assertTrue("Hello\n" in out) - self.assertTrue(textwrap.dedent("""\ + self.assertIn("Hello\n", out) + self.assertIn(textwrap.dedent("""\ Coverage.py warning: Module sys has no Python source. Coverage.py warning: Module xyzzy was never imported. Coverage.py warning: Module quux was never imported. Coverage.py warning: No data was collected. - """) in out) + """), out) def test_warnings_if_never_run(self): out = self.run_command("coverage run i_dont_exist.py") - self.assertTrue("No file to run: 'i_dont_exist.py'" in out) - self.assertTrue("warning" not in out) + self.assertIn("No file to run: 'i_dont_exist.py'", out) + self.assertNotIn("warning", out) + self.assertNotIn("Exception", out) out = self.run_command("coverage run -m no_such_module") self.assertTrue( ("No module named no_such_module" in out) or ("No module named 'no_such_module'" in out) ) - self.assertTrue("warning" not in out) + self.assertNotIn("warning", out) + self.assertNotIn("Exception", out) if sys.version_info >= (3, 0): # This only works on 3.x for now. # It only works with the C tracer. @@ -382,3 +393,59 @@ class ProcessTest(CoverageTest): # imported is 120 or so. Just running os.getenv executes # about 5. self.assertGreater(data.summary()['os.py'], 50) + + +class AliasedCommandTests(CoverageTest): + """Tests of the version-specific command aliases.""" + + def test__major_version_works(self): + # "coverage2" works on py2 + cmd = "coverage%d" % sys.version_info[0] + out = self.run_command(cmd) + self.assertIn("Code coverage for Python", out) + + def test_wrong_alias_doesnt_work(self): + # "coverage3" doesn't work on py2 + badcmd = "coverage%d" % (5 - sys.version_info[0]) + out = self.run_command(badcmd) + self.assertNotIn("Code coverage for Python", out) + + def test_specific_alias_works(self): + # "coverage-2.7" works on py2.7 + cmd = "coverage-%d.%d" % sys.version_info[:2] + out = self.run_command(cmd) + self.assertIn("Code coverage for Python", out) + + +class FailUnderTest(CoverageTest): + """Tests of the --fail-under switch.""" + + def setUp(self): + super(FailUnderTest, self).setUp() + self.make_file("fifty.py", """\ + # I have 50% coverage! + a = 1 + if a > 2: + b = 3 + c = 4 + """) + st, _ = self.run_command_status("coverage run fifty.py", 0) + self.assertEqual(st, 0) + + def test_report(self): + st, _ = self.run_command_status("coverage report --fail-under=50", 0) + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage report --fail-under=51", 2) + self.assertEqual(st, 2) + + def test_html_report(self): + st, _ = self.run_command_status("coverage html --fail-under=50", 0) + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage html --fail-under=51", 2) + self.assertEqual(st, 2) + + def test_xml_report(self): + st, _ = self.run_command_status("coverage xml --fail-under=50", 0) + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage xml --fail-under=51", 2) + self.assertEqual(st, 2) diff --git a/test/test_summary.py b/test/test_summary.py index 71fbb1a6..b460c2dc 100644 --- a/test/test_summary.py +++ b/test/test_summary.py @@ -25,7 +25,7 @@ class SummaryTest(CoverageTest): def report_from_command(self, cmd): """Return the report from the `cmd`, with some convenience added.""" report = self.run_command(cmd).replace('\\', '/') - self.assertFalse("error" in report.lower()) + self.assertNotIn("error", report.lower()) return report def line_count(self, report): @@ -51,10 +51,10 @@ class SummaryTest(CoverageTest): # --------------------------------------------------------------------- # TOTAL 8 0 100% - self.assertFalse("/coverage/__init__/" in report) - self.assertTrue("/test/modules/covmod1 " in report) - self.assertTrue("/test/zipmods.zip/covmodzip1 " in report) - self.assertTrue("mycode " in report) + self.assertNotIn("/coverage/__init__/", report) + self.assertIn("/test/modules/covmod1 ", report) + self.assertIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) self.assertEqual(self.last_line_squeezed(report), "TOTAL 8 0 100%") def test_report_just_one(self): @@ -67,10 +67,10 @@ class SummaryTest(CoverageTest): # mycode 4 0 100% self.assertEqual(self.line_count(report), 3) - self.assertFalse("/coverage/" in report) - self.assertFalse("/test/modules/covmod1 " in report) - self.assertFalse("/test/zipmods.zip/covmodzip1 " in report) - self.assertTrue("mycode " in report) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/test/modules/covmod1 ", report) + self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") def test_report_omitting(self): @@ -84,10 +84,10 @@ class SummaryTest(CoverageTest): # mycode 4 0 100% self.assertEqual(self.line_count(report), 3) - self.assertFalse("/coverage/" in report) - self.assertFalse("/test/modules/covmod1 " in report) - self.assertFalse("/test/zipmods.zip/covmodzip1 " in report) - self.assertTrue("mycode " in report) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/test/modules/covmod1 ", report) + self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") def test_report_including(self): @@ -100,10 +100,10 @@ class SummaryTest(CoverageTest): # mycode 4 0 100% self.assertEqual(self.line_count(report), 3) - self.assertFalse("/coverage/" in report) - self.assertFalse("/test/modules/covmod1 " in report) - self.assertFalse("/test/zipmods.zip/covmodzip1 " in report) - self.assertTrue("mycode " in report) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/test/modules/covmod1 ", report) + self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") def test_report_branches(self): @@ -118,12 +118,12 @@ class SummaryTest(CoverageTest): self.assertEqual(out, 'x\n') report = self.report_from_command("coverage report") - # Name Stmts Miss Branch BrPart Cover + # Name Stmts Miss Branch BrMiss Cover # -------------------------------------------- # mybranch 5 0 2 1 85% self.assertEqual(self.line_count(report), 3) - self.assertTrue("mybranch " in report) + self.assertIn("mybranch ", report) self.assertEqual(self.last_line_squeezed(report), "mybranch 5 0 2 1 86%") @@ -135,15 +135,16 @@ class SummaryTest(CoverageTest): self.make_file("mycode.py", "This isn't python at all!") report = self.report_from_command("coverage -r mycode.py") + # pylint: disable=C0301 # Name Stmts Miss Cover # ---------------------------- # mycode NotPython: Couldn't parse '/tmp/test_cover/63354509363/mycode.py' as Python source: 'invalid syntax' at line 1 last = self.last_line_squeezed(report) # The actual file name varies run to run. - last = re.sub("parse '.*mycode.py", "parse 'mycode.py", last) + last = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", last) # The actual error message varies version to version - last = re.sub(": '.*' at", ": 'error' at", last) + last = re.sub(r": '.*' at", ": 'error' at", last) self.assertEqual(last, "mycode NotPython: " "Couldn't parse 'mycode.py' as Python source: " @@ -178,6 +179,58 @@ class SummaryTest(CoverageTest): self.assertEqual(self.line_count(report), 2) + def get_report(self, cov): + """Get the report from `cov`, and canonicalize it.""" + repout = StringIO() + cov.report(file=repout, show_missing=False) + report = repout.getvalue().replace('\\', '/') + report = re.sub(r" +", " ", report) + return report + + def test_bug_156_file_not_run_should_be_zero(self): + # https://bitbucket.org/ned/coveragepy/issue/156 + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + self.make_file("main.py", """\ + print("y") + """) + cov = coverage.coverage(branch=True, source=["."]) + cov.start() + import main # pylint: disable=F0401,W0612 + cov.stop() + report = self.get_report(cov).splitlines() + self.assertIn("mybranch 5 5 2 2 0%", report) + + def run_TheCode_and_report_it(self): + """A helper for the next few tests.""" + cov = coverage.coverage() + cov.start() + import TheCode # pylint: disable=F0401,W0612 + cov.stop() + return self.get_report(cov) + + def test_bug_203_mixed_case_listed_twice_with_rc(self): + self.make_file("TheCode.py", "a = 1\n") + self.make_file(".coveragerc", "[run]\nsource = .\n") + + report = self.run_TheCode_and_report_it() + + self.assertIn("TheCode", report) + self.assertNotIn("thecode", report) + + def test_bug_203_mixed_case_listed_twice(self): + self.make_file("TheCode.py", "a = 1\n") + + report = self.run_TheCode_and_report_it() + + self.assertIn("TheCode", report) + self.assertNotIn("thecode", report) + class SummaryTest2(CoverageTest): """Another bunch of summary tests.""" @@ -207,5 +260,42 @@ class SummaryTest2(CoverageTest): report = repout.getvalue().replace('\\', '/') report = re.sub(r"\s+", " ", report) - self.assert_("test/modules/pkg1/__init__ 1 0 100%" in report) - self.assert_("test/modules/pkg2/__init__ 0 0 100%" in report) + self.assertIn("test/modules/pkg1/__init__ 1 0 100%", report) + self.assertIn("test/modules/pkg2/__init__ 0 0 100%", report) + + +class ReportingReturnValue(CoverageTest): + """Tests of reporting functions returning values.""" + + def run_coverage(self): + """Run coverage on doit.py and return the coverage object.""" + self.make_file("doit.py", """\ + a = 1 + b = 2 + c = 3 + d = 4 + if a > 10: + f = 6 + g = 7 + """) + + cov = coverage.coverage() + cov.start() + self.import_local_file("doit") + cov.stop() + return cov + + def test_report(self): + cov = self.run_coverage() + val = cov.report(include="*/doit.py") + self.assertAlmostEqual(val, 85.7, 1) + + def test_html(self): + cov = self.run_coverage() + val = cov.html_report(include="*/doit.py") + self.assertAlmostEqual(val, 85.7, 1) + + def test_xml(self): + cov = self.run_coverage() + val = cov.xml_report(include="*/doit.py") + self.assertAlmostEqual(val, 85.7, 1) diff --git a/test/test_testing.py b/test/test_testing.py index 316dbc1b..9943b65c 100644 --- a/test/test_testing.py +++ b/test/test_testing.py @@ -1,7 +1,9 @@ +# -*- coding: utf-8 -*- """Tests that our test infrastructure is really working!""" import os, sys sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k +from coverage.backward import to_bytes from backunittest import TestCase from coveragetest import CoverageTest @@ -92,6 +94,34 @@ class TestingTest(TestCase): self.assertFalse(False) self.assertRaises(AssertionError, self.assertFalse, True) + def test_assert_in(self): + self.assertIn("abc", "hello abc") + self.assertIn("abc", ["xyz", "abc", "foo"]) + self.assertIn("abc", {'abc': 1, 'xyz': 2}) + self.assertRaises(AssertionError, self.assertIn, "abc", "xyz") + self.assertRaises(AssertionError, self.assertIn, "abc", ["x", "xabc"]) + self.assertRaises(AssertionError, self.assertIn, "abc", {'x':'abc'}) + + def test_assert_not_in(self): + self.assertRaises(AssertionError, self.assertNotIn, "abc", "hello abc") + self.assertRaises(AssertionError, + self.assertNotIn, "abc", ["xyz", "abc", "foo"] + ) + self.assertRaises(AssertionError, + self.assertNotIn, "abc", {'abc': 1, 'xyz': 2} + ) + self.assertNotIn("abc", "xyz") + self.assertNotIn("abc", ["x", "xabc"]) + self.assertNotIn("abc", {'x':'abc'}) + + def test_assert_greater(self): + self.assertGreater(10, 9) + self.assertGreater("xyz", "abc") + self.assertRaises(AssertionError, self.assertGreater, 9, 10) + self.assertRaises(AssertionError, self.assertGreater, 10, 10) + self.assertRaises(AssertionError, self.assertGreater, "abc", "xyz") + self.assertRaises(AssertionError, self.assertGreater, "xyz", "xyz") + class CoverageTestTest(CoverageTest): """Test the methods in `CoverageTest`.""" @@ -122,10 +152,18 @@ class CoverageTestTest(CoverageTest): self.make_file("mac.txt", "Hello\n", newline="\r") self.assertEqual(self.file_text("mac.txt"), "Hello\r") + def test_make_file_non_ascii(self): + self.make_file("unicode.txt", "tabblo: «ταБЬℓσ»") + self.assertEqual( + open("unicode.txt", "rb").read(), + to_bytes("tabblo: «ταБЬℓσ»") + ) + def test_file_exists(self): self.make_file("whoville.txt", "We are here!") self.assert_exists("whoville.txt") self.assert_doesnt_exist("shadow.txt") - self.assertRaises(AssertionError, self.assert_doesnt_exist, - "whoville.txt") + self.assertRaises( + AssertionError, self.assert_doesnt_exist, "whoville.txt" + ) self.assertRaises(AssertionError, self.assert_exists, "shadow.txt") diff --git a/test/test_xml.py b/test/test_xml.py new file mode 100644 index 00000000..6542dcac --- /dev/null +++ b/test/test_xml.py @@ -0,0 +1,89 @@ +"""Tests for XML reports from coverage.py.""" + +import os, re, sys +import coverage + +sys.path.insert(0, os.path.split(__file__)[0]) # Force relative import for Py3k +from coveragetest import CoverageTest + +class XmlReportTest(CoverageTest): + """Tests of the XML reports from coverage.py.""" + + def run_mycode(self): + """Run mycode.py, so we can report on it.""" + self.make_file("mycode.py", "print('hello')\n") + self.run_command("coverage run mycode.py") + + def test_default_file_placement(self): + self.run_mycode() + self.run_command("coverage xml") + self.assert_exists("coverage.xml") + + def test_argument_affects_xml_placement(self): + self.run_mycode() + self.run_command("coverage xml -o put_it_there.xml") + self.assert_doesnt_exist("coverage.xml") + self.assert_exists("put_it_there.xml") + + def test_config_affects_xml_placement(self): + self.run_mycode() + self.make_file(".coveragerc", "[xml]\noutput = xml.out\n") + self.run_command("coverage xml") + self.assert_doesnt_exist("coverage.xml") + self.assert_exists("xml.out") + + def test_no_data(self): + # https://bitbucket.org/ned/coveragepy/issue/210 + self.run_command("coverage xml") + self.assert_doesnt_exist("coverage.xml") + + def test_no_source(self): + # Written while investigating a bug, might as well keep it. + # https://bitbucket.org/ned/coveragepy/issue/208 + self.make_file("innocuous.py", "a = 1") + cov = coverage.coverage() + cov.start() + self.import_local_file("innocuous") + cov.stop() + os.remove("innocuous.py") + cov.xml_report(ignore_errors=True) + self.assert_exists("coverage.xml") + + def run_doit(self): + """Construct a simple sub-package.""" + self.make_file("sub/__init__.py") + self.make_file("sub/doit.py", "print('doit!')") + self.make_file("main.py", "import sub.doit") + cov = coverage.coverage() + cov.start() + self.import_local_file("main") + cov.stop() + return cov + + def test_filename_format_showing_everything(self): + cov = self.run_doit() + cov.xml_report(outfile="-") + xml = self.stdout() + doit_line = re_line(xml, "class.*doit") + self.assertIn('filename="sub/doit.py"', doit_line) + + def test_filename_format_including_filename(self): + cov = self.run_doit() + cov.xml_report(["sub/doit.py"], outfile="-") + xml = self.stdout() + doit_line = re_line(xml, "class.*doit") + self.assertIn('filename="sub/doit.py"', doit_line) + + def test_filename_format_including_module(self): + cov = self.run_doit() + import sub.doit + cov.xml_report([sub.doit], outfile="-") + xml = self.stdout() + doit_line = re_line(xml, "class.*doit") + self.assertIn('filename="sub/doit.py"', doit_line) + + +def re_line(text, pat): + """Return the one line in `text` that matches regex `pat`.""" + lines = [l for l in text.splitlines() if re.search(pat, l)] + return lines[0] @@ -11,8 +11,8 @@ setenv = PYTHONPATH=test/eggsrc commands = - {envpython} setup.py clean - {envpython} setup.py develop + {envpython} setup.py --quiet clean + {envpython} setup.py --quiet develop # Create test/zipmods.zip {envpython} igor.py zip_mods @@ -24,7 +24,7 @@ commands = {envpython} igor.py test_with_tracer py {posargs} # Build the C extension and test with the CTracer - {envpython} setup.py build_ext --inplace + {envpython} setup.py --quiet build_ext --inplace {envpython} igor.py test_with_tracer c {posargs} deps = |
