diff options
author | Danny Allen <me@dannya.com> | 2014-08-11 16:13:06 +0100 |
---|---|---|
committer | Danny Allen <me@dannya.com> | 2014-08-11 16:13:06 +0100 |
commit | e38016c499921dd7bf5919a699a76305a1936129 (patch) | |
tree | 07a4125732561f2489dfb6b75a339cfef46d80d4 | |
parent | c81183f614ca982cd2ed93ac8e6e76610d162202 (diff) | |
parent | ee5ea987f8978d91c1ef189fe4f334511ddf6215 (diff) | |
download | python-coveragepy-git-e38016c499921dd7bf5919a699a76305a1936129.tar.gz |
Merged ned/coveragepy into default
54 files changed, 1255 insertions, 578 deletions
diff --git a/AUTHORS.txt b/AUTHORS.txt index d7e2d276..3aa04adf 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -24,6 +24,7 @@ Roger Hu Stan Hu Devin Jeanpierre Ross Lawley +Steve Leonard Edward Loper Sandra Martocchia Patrick Mezard @@ -37,6 +38,7 @@ Adi Roiban Greg Rogers Chris Rose George Song +Anthony Sottile David Stanek Joseph Tate Sigve Tjora diff --git a/CHANGES.txt b/CHANGES.txt index 26bd45cb..539037a5 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,14 +8,42 @@ Change history for Coverage.py - Python versions supported are now CPython 2.6, 2.7, 3.2, 3.3, and 3.4, and PyPy 2.2. +- Options are now also read from a setup.cfg file, if any. Sections are + prefixed with "coverage:", so the ``[run]`` options will be read from the + ``[coverage:run]`` section of setup.cfg. Finishes `issue 304`_. + +- The ``report`` command can now show missing branches when reporting on branch + coverage. Thanks, Steve Leonard. Closes `issue 230`. + - The XML report now contains a <source> element, fixing `issue 94`_. Thanks Stan Hu. +- The ``fail-under`` value is now rounded the same as reported results, + preventing paradoxical results, fixing `issue 284`_. + - The XML report will now create the output directory if need be, fixing `issue 285`_. Thanks Chris Rose. +- HTML reports no longer raise UnicodeDecodeError if a Python file has + undecodable characters, fixing `issue 303`_. + +- The annotate command will now annotate all files, not just ones relative to + the current directory, fixing `issue 57`_. + +- The coverage module no longer causes deprecation warnings on Python 3.4 by + importing the imp module, fixing `issue 305`_. + +- Encoding declarations in source files are only considered if they are truly + comments. Thanks, Anthony Sottile. + +.. _issue 57: https://bitbucket.org/ned/coveragepy/issue/57/annotate-command-fails-to-annotate-many .. _issue 94: https://bitbucket.org/ned/coveragepy/issue/94/coverage-xml-doesnt-produce-sources +.. _issue 230: https://bitbucket.org/ned/coveragepy/issue/230/show-line-no-for-missing-branches-in +.. _issue 284: https://bitbucket.org/ned/coveragepy/issue/284/fail-under-should-show-more-precision .. _issue 285: https://bitbucket.org/ned/coveragepy/issue/285/xml-report-fails-if-output-file-directory +.. _issue 303: https://bitbucket.org/ned/coveragepy/issue/303/unicodedecodeerror +.. _issue 304: https://bitbucket.org/ned/coveragepy/issue/304/attempt-to-get-configuration-from-setupcfg +.. _issue 305: https://bitbucket.org/ned/coveragepy/issue/305/pendingdeprecationwarning-the-imp-module 3.7.1 -- 13 December 2013 diff --git a/coverage/annotate.py b/coverage/annotate.py index 19777eaf..5b96448a 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -47,55 +47,44 @@ class AnnotateReporter(Reporter): `cu` is the CodeUnit for the file to annotate. """ - if not cu.relative: - return + statements = sorted(analysis.statements) + missing = sorted(analysis.missing) + excluded = sorted(analysis.excluded) - filename = cu.filename - source = cu.source_file() if self.directory: dest_file = os.path.join(self.directory, cu.flat_rootname()) dest_file += ".py,cover" else: - dest_file = filename + ",cover" - dest = open(dest_file, 'w') - - statements = sorted(analysis.statements) - missing = sorted(analysis.missing) - excluded = sorted(analysis.excluded) - - lineno = 0 - i = 0 - j = 0 - covered = True - while True: - line = source.readline() - if line == '': - break - lineno += 1 - while i < len(statements) and statements[i] < lineno: - i += 1 - while j < len(missing) and missing[j] < lineno: - j += 1 - if i < len(statements) and statements[i] == lineno: - covered = j >= len(missing) or missing[j] > lineno - if self.blank_re.match(line): - dest.write(' ') - elif self.else_re.match(line): - # Special logic for lines containing only 'else:'. - if i >= len(statements) and j >= len(missing): - dest.write('! ') - elif i >= len(statements) or j >= len(missing): + dest_file = cu.filename + ",cover" + + with open(dest_file, 'w') as dest: + i = 0 + j = 0 + covered = True + source = cu.source() + for lineno, line in enumerate(source.splitlines(True), start=1): + while i < len(statements) and statements[i] < lineno: + i += 1 + while j < len(missing) and missing[j] < lineno: + j += 1 + if i < len(statements) and statements[i] == lineno: + covered = j >= len(missing) or missing[j] > lineno + if self.blank_re.match(line): + dest.write(' ') + elif self.else_re.match(line): + # Special logic for lines containing only 'else:'. + if i >= len(statements) and j >= len(missing): + dest.write('! ') + elif i >= len(statements) or j >= len(missing): + dest.write('> ') + elif statements[i] == missing[j]: + dest.write('! ') + else: + dest.write('> ') + elif lineno in excluded: + dest.write('- ') + elif covered: dest.write('> ') - elif statements[i] == missing[j]: - dest.write('! ') else: - dest.write('> ') - elif lineno in excluded: - dest.write('- ') - elif covered: - dest.write('> ') - else: - dest.write('! ') - dest.write(line) - source.close() - dest.close() + dest.write('! ') + dest.write(line) diff --git a/coverage/backward.py b/coverage/backward.py index e81dd199..a7888a24 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -1,10 +1,11 @@ """Add things to old Pythons so I can pretend they are newer.""" # This file does lots of tricky stuff, so disable a bunch of lintisms. -# pylint: disable=F0401,W0611,W0622 -# F0401: Unable to import blah -# W0611: Unused import blah -# W0622: Redefining built-in blah +# pylint: disable=redefined-builtin +# pylint: disable=import-error +# pylint: disable=no-member +# pylint: disable=unused-import +# pylint: disable=no-name-in-module import os, re, sys @@ -124,3 +125,59 @@ try: except ImportError: import md5 md5 = md5.new + + +try: + # In Py 2.x, the builtins were in __builtin__ + BUILTINS = sys.modules['__builtin__'] +except KeyError: + # In Py 3.x, they're in builtins + BUILTINS = sys.modules['builtins'] + + +# imp was deprecated in Python 3.3 +try: + import importlib, importlib.util + imp = None +except ImportError: + importlib = None + +# we only want to use importlib if it has everything we need. +try: + importlib_util_find_spec = importlib.util.find_spec +except Exception: + import imp + importlib_util_find_spec = None + +try: + PYC_MAGIC_NUMBER = importlib.util.MAGIC_NUMBER +except AttributeError: + PYC_MAGIC_NUMBER = imp.get_magic() + + +def import_local_file(modname): + """Import a local file as a module. + + Opens a file in the current directory named `modname`.py, imports it + as `modname`, and returns the module object. + + """ + try: + from importlib.machinery import SourceFileLoader + except ImportError: + SourceFileLoader = None + + modfile = modname + '.py' + if SourceFileLoader: + mod = SourceFileLoader(modname, modfile).load_module() + else: + for suff in imp.get_suffixes(): + if suff[0] == '.py': + break + + with open(modfile, 'r') as f: + # pylint: disable=W0631 + # (Using possibly undefined loop variable 'suff') + mod = imp.load_module(modname, f, modfile, suff) + + return mod diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 19e0536e..bd10d5a8 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -1,6 +1,6 @@ """Command-line support for Coverage.""" -import optparse, os, sys, time, traceback +import glob, optparse, os, sys, time, traceback from coverage.execfile import run_python_file, run_python_module from coverage.misc import CoverageException, ExceptionDuringRun, NoSource @@ -449,7 +449,7 @@ class CoverageScript(object): # Remaining actions are reporting, with some common options. report_args = dict( - morfs = args, + morfs = unglob_args(args), ignore_errors = options.ignore_errors, omit = omit, include = include, @@ -470,6 +470,14 @@ class CoverageScript(object): total = self.coverage.xml_report(outfile=outfile, **report_args) if options.fail_under is not None: + # Total needs to be rounded, but be careful of 0 and 100. + if 0 < total < 1: + total = 1 + elif 99 < total < 100: + total = 99 + else: + total = round(total) + if total >= options.fail_under: return OK else: @@ -633,6 +641,19 @@ def unshell_list(s): return s.split(',') +def unglob_args(args): + """Interpret shell wildcards for platforms that need it.""" + if sys.platform == 'win32': + globbed = [] + for arg in args: + if '?' in arg or '*' in arg: + globbed.extend(glob.glob(arg)) + else: + globbed.append(arg) + args = globbed + return args + + HELP_TOPICS = { # ------------------------- 'classic': diff --git a/coverage/codeunit.py b/coverage/codeunit.py index 88858801..35167a72 100644 --- a/coverage/codeunit.py +++ b/coverage/codeunit.py @@ -1,20 +1,24 @@ """Code unit (module) handling for Coverage.""" -import glob, os, re +import os -from coverage.backward import open_python_source, string_class, StringIO +from coverage.backward import open_python_source, string_class from coverage.misc import CoverageException, NoSource from coverage.parser import CodeParser, PythonParser from coverage.phystokens import source_token_lines, source_encoding +from coverage.django import DjangoTracer -def code_unit_factory(morfs, file_locator): + +def code_unit_factory(morfs, file_locator, get_ext=None): """Construct a list of CodeUnits from polymorphic inputs. `morfs` is a module or a filename, or a list of same. `file_locator` is a FileLocator that can help resolve filenames. + `get_ext` TODO + Returns a list of CodeUnit objects. """ @@ -22,25 +26,28 @@ def code_unit_factory(morfs, file_locator): if not isinstance(morfs, (list, tuple)): morfs = [morfs] - # On Windows, the shell doesn't expand wildcards. Do it here. - globbed = [] - for morf in morfs: - if isinstance(morf, string_class) and ('?' in morf or '*' in morf): - globbed.extend(glob.glob(morf)) - else: - globbed.append(morf) - morfs = globbed + django_tracer = DjangoTracer() code_units = [] for morf in morfs: - # Hacked-in Mako support. Disabled for going onto trunk. - if 0 and isinstance(morf, string_class) and "/mako/" in morf: - # Super hack! Do mako both ways! - if 0: - cu = PythonCodeUnit(morf, file_locator) - cu.name += '_fako' - code_units.append(cu) - klass = MakoCodeUnit + ext = None + if isinstance(morf, string_class) and get_ext: + ext = get_ext(morf) + if ext: + klass = DjangoTracer # NOT REALLY! TODO + # Hacked-in Mako support. Define COVERAGE_MAKO_PATH as a fragment of + # the path that indicates the Python file is actually a compiled Mako + # template. THIS IS TEMPORARY! + #MAKO_PATH = os.environ.get('COVERAGE_MAKO_PATH') + #if MAKO_PATH and isinstance(morf, string_class) and MAKO_PATH in morf: + # # Super hack! Do mako both ways! + # if 0: + # cu = PythonCodeUnit(morf, file_locator) + # cu.name += '_fako' + # code_units.append(cu) + # klass = MakoCodeUnit + #elif isinstance(morf, string_class) and morf.endswith(".html"): + # klass = DjangoCodeUnit else: klass = PythonCodeUnit code_units.append(klass(morf, file_locator)) @@ -87,6 +94,10 @@ class CodeUnit(object): def __repr__(self): return "<CodeUnit name=%r filename=%r>" % (self.name, self.filename) + def _adjust_filename(self, f): + # TODO: This shouldn't be in the base class, right? + return f + # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all # of them defined. @@ -119,22 +130,29 @@ class CodeUnit(object): root = os.path.splitdrive(self.name)[1] return root.replace('\\', '_').replace('/', '_').replace('.', '_') - def source_file(self): - """Return an open file for reading the source of the code unit.""" + def source(self): + """Return the source code, as a string.""" if os.path.exists(self.filename): # A regular text file: open it. - return open_python_source(self.filename) + with open_python_source(self.filename) as f: + return f.read() # Maybe it's in a zip file? source = self.file_locator.get_zip_data(self.filename) if source is not None: - return StringIO(source) + return source # Couldn't find source. raise CoverageException( "No source for code '%s'." % self.filename ) + def source_token_lines(self, source): + """Return the 'tokenized' text for the code.""" + # TODO: Taking source here is wrong, change it? + for line in source.splitlines(): + yield [('txt', line)] + def should_be_python(self): """Does it seem like this file should contain Python? @@ -148,8 +166,6 @@ class CodeUnit(object): class PythonCodeUnit(CodeUnit): """Represents a Python file.""" - parser_class = PythonParser - def _adjust_filename(self, fname): # .pyc files should always refer to a .py instead. if fname.endswith(('.pyc', '.pyo')): @@ -158,7 +174,13 @@ class PythonCodeUnit(CodeUnit): fname = fname[:-9] + ".py" return fname - def find_source(self, filename): + def get_parser(self, exclude=None): + actual_filename, source = self._find_source(self.filename) + return PythonParser( + text=source, filename=actual_filename, exclude=exclude, + ) + + def _find_source(self, filename): """Find the source for `filename`. Returns two values: the actual filename, and the source. @@ -223,78 +245,61 @@ class PythonCodeUnit(CodeUnit): return source_encoding(source) -def mako_template_name(py_filename): - with open(py_filename) as f: - py_source = f.read() - - # Find the template filename. TODO: string escapes in the string. - m = re.search(r"^_template_filename = u?'([^']+)'", py_source, flags=re.MULTILINE) - if not m: - raise Exception("Couldn't find template filename in Mako file %r" % py_filename) - template_filename = m.group(1) - return template_filename - - class MakoParser(CodeParser): - def __init__(self, cu, text, filename, exclude): - self.cu = cu - self.text = text - self.filename = filename - self.exclude = exclude + def __init__(self, metadata): + self.metadata = metadata def parse_source(self): """Returns executable_line_numbers, excluded_line_numbers""" - with open(self.cu.filename) as f: - py_source = f.read() - - # Get the line numbers. - self.py_to_html = {} - html_linenum = None - for linenum, line in enumerate(py_source.splitlines(), start=1): - m_source_line = re.search(r"^\s*# SOURCE LINE (\d+)$", line) - if m_source_line: - html_linenum = int(m_source_line.group(1)) - else: - m_boilerplate_line = re.search(r"^\s*# BOILERPLATE", line) - if m_boilerplate_line: - html_linenum = None - elif html_linenum: - self.py_to_html[linenum] = html_linenum - - return set(self.py_to_html.values()), set() + executable = set(self.metadata['line_map'].values()) + return executable, set() def translate_lines(self, lines): - tlines = set(self.py_to_html.get(l, -1) for l in lines) - tlines.remove(-1) + tlines = set() + for l in lines: + try: + tlines.add(self.metadata['full_line_map'][l]) + except IndexError: + pass return tlines class MakoCodeUnit(CodeUnit): - parser_class = MakoParser - def __init__(self, *args, **kwargs): super(MakoCodeUnit, self).__init__(*args, **kwargs) - self.mako_filename = mako_template_name(self.filename) + from mako.template import ModuleInfo + py_source = open(self.filename).read() + self.metadata = ModuleInfo.get_module_source_metadata(py_source, full_line_map=True) - def source_file(self): - return open(self.mako_filename) + def source(self): + return open(self.metadata['filename']).read() - def find_source(self, filename): - """Find the source for `filename`. + def get_parser(self, exclude=None): + return MakoParser(self.metadata) - Returns two values: the actual filename, and the source. + def source_encoding(self, source): + # TODO: Taking source here is wrong, change it! + return self.metadata['source_encoding'] - """ - mako_filename = mako_template_name(filename) - with open(mako_filename) as f: - source = f.read() - return mako_filename, source +class DjangoCodeUnit(CodeUnit): + def source(self): + with open(self.filename) as f: + return f.read() - def source_token_lines(self, source): - """Return the 'tokenized' text for the code.""" - for line in source.splitlines(): - yield [('txt', line)] + def get_parser(self, exclude=None): + return DjangoParser(self.filename) def source_encoding(self, source): - return "utf-8" + return "utf8" + + +class DjangoParser(CodeParser): + def __init__(self, filename): + self.filename = filename + + def parse_source(self): + with open(self.filename) as f: + source = f.read() + executable = set(range(1, len(source.splitlines())+1)) + return executable, set() diff --git a/coverage/collector.py b/coverage/collector.py index 94af5df5..546525d2 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -41,17 +41,22 @@ class PyTracer(object): # used to force the use of this tracer. def __init__(self): + # Attributes set from the collector: self.data = None + self.arcs = False self.should_trace = None self.should_trace_cache = None self.warn = None + self.extensions = None + + self.extension = None + self.cur_tracename = None # TODO: This is only maintained for the if0 debugging output. Get rid of it eventually. self.cur_file_data = None self.last_line = 0 self.data_stack = [] self.data_stacks = collections.defaultdict(list) self.last_exc_back = None self.last_exc_firstlineno = 0 - self.arcs = False self.thread = None self.stopped = False self.coroutine_id_func = None @@ -64,9 +69,31 @@ class PyTracer(object): return if 0: - sys.stderr.write("trace event: %s %r @%d\n" % ( - event, frame.f_code.co_filename, frame.f_lineno + # A lot of debugging to try to understand why gevent isn't right. + import os.path, pprint + def short_ident(ident): + return "{}:{:06X}".format(ident.__class__.__name__, id(ident) & 0xFFFFFF) + + ident = None + if self.coroutine_id_func: + ident = short_ident(self.coroutine_id_func()) + sys.stdout.write("trace event: %s %s %r @%d\n" % ( + event, ident, frame.f_code.co_filename, frame.f_lineno )) + pprint.pprint( + dict( + ( + short_ident(ident), + [ + (os.path.basename(tn or ""), sorted((cfd or {}).keys()), ll) + for ex, tn, cfd, ll in data_stacks + ] + ) + for ident, data_stacks in self.data_stacks.items() + ) + , width=250) + pprint.pprint(sorted((self.cur_file_data or {}).keys()), width=250) + print("TRYING: {}".format(sorted(next((v for k,v in self.data.items() if k.endswith("try_it.py")), {}).keys()))) if self.last_exc_back: if frame == self.last_exc_back: @@ -76,7 +103,7 @@ class PyTracer(object): self.cur_file_data[pair] = None if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] - self.cur_file_data, self.last_line = self.data_stack.pop() + self.handler, _, self.cur_file_data, self.last_line = self.data_stack.pop() self.last_exc_back = None if event == 'call': @@ -85,19 +112,25 @@ class PyTracer(object): if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] self.last_coroutine = self.coroutine_id_func() - self.data_stack.append((self.cur_file_data, self.last_line)) + self.data_stack.append((self.extension, self.cur_tracename, self.cur_file_data, self.last_line)) filename = frame.f_code.co_filename - if filename not in self.should_trace_cache: - tracename = self.should_trace(filename, frame) - self.should_trace_cache[filename] = tracename - else: - tracename = self.should_trace_cache[filename] + disp = self.should_trace_cache.get(filename) + if disp is None: + disp = self.should_trace(filename, frame) + self.should_trace_cache[filename] = disp #print("called, stack is %d deep, tracename is %r" % ( # len(self.data_stack), tracename)) + tracename = disp.filename + if tracename and disp.extension: + tracename = disp.extension.file_name(frame) if tracename: if tracename not in self.data: self.data[tracename] = {} + if disp.extension: + self.extensions[tracename] = disp.extension.__name__ + self.cur_tracename = tracename self.cur_file_data = self.data[tracename] + self.extension = disp.extension else: self.cur_file_data = None # Set the last_line to -1 because the next arc will be entering a @@ -105,16 +138,24 @@ class PyTracer(object): self.last_line = -1 elif event == 'line': # Record an executed line. - #if self.coroutine_id_func: - # assert self.last_coroutine == self.coroutine_id_func() - if self.cur_file_data is not None: - if self.arcs: - #print("lin", self.last_line, frame.f_lineno) - self.cur_file_data[(self.last_line, frame.f_lineno)] = None - else: - #print("lin", frame.f_lineno) - self.cur_file_data[frame.f_lineno] = None - self.last_line = frame.f_lineno + if 0 and self.coroutine_id_func: + this_coroutine = self.coroutine_id_func() + if self.last_coroutine != this_coroutine: + print("mismatch: {0} != {1}".format(self.last_coroutine, this_coroutine)) + if self.extension: + lineno_from, lineno_to = self.extension.line_number_range(frame) + else: + lineno_from, lineno_to = frame.f_lineno, frame.f_lineno + if lineno_from != -1: + if self.cur_file_data is not None: + if self.arcs: + #print("lin", self.last_line, frame.f_lineno) + self.cur_file_data[(self.last_line, lineno_from)] = None + else: + #print("lin", frame.f_lineno) + for lineno in range(lineno_from, lineno_to+1): + self.cur_file_data[lineno] = None + self.last_line = lineno_to elif event == 'return': if self.arcs and self.cur_file_data: first = frame.f_code.co_firstlineno @@ -123,7 +164,7 @@ class PyTracer(object): if self.coroutine_id_func: self.data_stack = self.data_stacks[self.coroutine_id_func()] self.last_coroutine = self.coroutine_id_func() - self.cur_file_data, self.last_line = self.data_stack.pop() + self.extension, _, self.cur_file_data, self.last_line = self.data_stack.pop() #print("returned, stack is %d deep" % (len(self.data_stack))) elif event == 'exception': #print("exc", self.last_line, frame.f_lineno) @@ -240,6 +281,8 @@ class Collector(object): # or mapping filenames to dicts with linenumber pairs as keys. self.data = {} + self.extensions = {} + # A cache of the results from should_trace, the decision about whether # to trace execution in a file. A dict of filename to (filename or # None). @@ -258,6 +301,8 @@ class Collector(object): tracer.warn = self.warn if hasattr(tracer, 'coroutine_id_func'): tracer.coroutine_id_func = self.coroutine_id_func + if hasattr(tracer, 'extensions'): + tracer.extensions = self.extensions fn = tracer.start() self.tracers.append(tracer) return fn @@ -356,10 +401,7 @@ class Collector(object): # to show line data. line_data = {} for f, arcs in self.data.items(): - line_data[f] = ldf = {} - for l1, _ in list(arcs.keys()): - if l1: - ldf[l1] = None + line_data[f] = dict((l1, None) for l1, _ in arcs.keys() if l1) return line_data else: return self.data @@ -377,3 +419,6 @@ class Collector(object): return self.data else: return {} + + def get_extension_data(self): + return self.extensions diff --git a/coverage/config.py b/coverage/config.py index 60ec3f41..064bc1ca 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -13,6 +13,11 @@ except ImportError: class HandyConfigParser(configparser.RawConfigParser): """Our specialization of ConfigParser.""" + def __init__(self, section_prefix): + # pylint: disable=super-init-not-called + configparser.RawConfigParser.__init__(self) + self.section_prefix = section_prefix + def read(self, filename): """Read a filename as UTF-8 configuration data.""" kwargs = {} @@ -20,8 +25,30 @@ class HandyConfigParser(configparser.RawConfigParser): kwargs['encoding'] = "utf-8" return configparser.RawConfigParser.read(self, filename, **kwargs) - def get(self, *args, **kwargs): - v = configparser.RawConfigParser.get(self, *args, **kwargs) + def has_option(self, section, option): + section = self.section_prefix + section + return configparser.RawConfigParser.has_option(self, section, option) + + def has_section(self, section): + section = self.section_prefix + section + return configparser.RawConfigParser.has_section(self, section) + + def options(self, section): + section = self.section_prefix + section + return configparser.RawConfigParser.options(self, section) + + def get(self, section, *args, **kwargs): + """Get a value, replacing environment variables also. + + The arguments are the same as `RawConfigParser.get`, but in the found + value, ``$WORD`` or ``${WORD}`` are replaced by the value of the + environment variable ``WORD``. + + Returns the finished value. + + """ + section = self.section_prefix + section + v = configparser.RawConfigParser.get(self, section, *args, **kwargs) def dollar_replace(m): """Called for each $replacement.""" # Only one of the groups will have matched, just get its text. @@ -113,6 +140,7 @@ class CoverageConfig(object): self.timid = False self.source = None self.debug = [] + self.extensions = [] # Defaults for [report] self.exclude_list = DEFAULT_EXCLUDE[:] @@ -144,7 +172,7 @@ class CoverageConfig(object): if env: self.timid = ('--timid' in env) - MUST_BE_LIST = ["omit", "include", "debug"] + MUST_BE_LIST = ["omit", "include", "debug", "extensions"] def from_args(self, **kwargs): """Read config values from `kwargs`.""" @@ -154,18 +182,22 @@ class CoverageConfig(object): v = [v] setattr(self, k, v) - def from_file(self, filename): + def from_file(self, filename, section_prefix=""): """Read configuration from a .rc file. `filename` is a file name to read. + Returns True or False, whether the file could be read. + """ self.attempted_config_files.append(filename) - cp = HandyConfigParser() + cp = HandyConfigParser(section_prefix) files_read = cp.read(filename) - if files_read is not None: # return value changed in 2.4 - self.config_files.extend(files_read) + if not files_read: + return False + + self.config_files.extend(files_read) for option_spec in self.CONFIG_FILE_OPTIONS: self.set_attr_from_config_option(cp, *option_spec) @@ -175,13 +207,24 @@ class CoverageConfig(object): for option in cp.options('paths'): self.paths[option] = cp.getlist('paths', option) + return True + CONFIG_FILE_OPTIONS = [ + # These are *args for set_attr_from_config_option: + # (attr, where, type_="") + # + # attr is the attribute to set on the CoverageConfig object. + # where is the section:name to read from the configuration file. + # type_ is the optional type to apply, by using .getTYPE to read the + # configuration value from the file. + # [run] ('branch', 'run:branch', 'boolean'), ('coroutine', 'run:coroutine'), ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), + ('extensions', 'run:extensions', 'list'), ('include', 'run:include', 'list'), ('omit', 'run:omit', 'list'), ('parallel', 'run:parallel', 'boolean'), diff --git a/coverage/control.py b/coverage/control.py index 44a70bf0..cb917e52 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -9,6 +9,7 @@ from coverage.collector import Collector from coverage.config import CoverageConfig from coverage.data import CoverageData from coverage.debug import DebugControl +from coverage.extension import load_extensions from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.html import HtmlReporter @@ -18,6 +19,7 @@ from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter + # Pypy has some unusual stuff in the "stdlib". Consider those locations # when deciding where the stdlib is. try: @@ -97,17 +99,22 @@ class coverage(object): # 1: defaults: self.config = CoverageConfig() - # 2: from the coveragerc file: + # 2: from the .coveragerc or setup.cfg file: if config_file: + did_read_rc = should_read_setupcfg = False if config_file is True: config_file = ".coveragerc" + should_read_setupcfg = True try: - self.config.from_file(config_file) + did_read_rc = self.config.from_file(config_file) except ValueError as err: raise CoverageException( "Couldn't read config file %s: %s" % (config_file, err) ) + if not did_read_rc and should_read_setupcfg: + self.config.from_file("setup.cfg", section_prefix="coverage:") + # 3: from environment variables: self.config.from_environment('COVERAGE_OPTIONS') env_data_file = os.environ.get('COVERAGE_FILE') @@ -125,6 +132,10 @@ class coverage(object): # Create and configure the debugging controller. self.debug = DebugControl(self.config.debug, debug_file or sys.stderr) + # Load extensions + tracer_classes = load_extensions(self.config.extensions, "tracer") + self.tracer_extensions = [cls() for cls in tracer_classes] + self.auto_data = auto_data # _exclude_re is a dict mapping exclusion list names to compiled @@ -232,22 +243,24 @@ class coverage(object): This function is called from the trace function. As each new file name is encountered, this function determines whether it is traced or not. - Returns a pair of values: the first indicates whether the file should - be traced: it's a canonicalized filename if it should be traced, None - if it should not. The second value is a string, the resason for the - decision. + Returns a FileDisposition object. """ + disp = FileDisposition(filename) + if not filename: # Empty string is pretty useless - return None, "empty string isn't a filename" + return disp.nope("empty string isn't a filename") + + if filename.startswith('memory:'): + return disp.nope("memory isn't traceable") if filename.startswith('<'): # Lots of non-file execution is represented with artificial # filenames like "<string>", "<doctest readme.txt[0]>", or # "<exec_function>". Don't ever trace these executions, since we # can't do anything with the data later anyway. - return None, "not a real filename" + return disp.nope("not a real filename") self._check_for_packages() @@ -267,47 +280,51 @@ class coverage(object): canonical = self.file_locator.canonical_filename(filename) + # Try the extensions, see if they have an opinion about the file. + for tracer in self.tracer_extensions: + ext_disp = tracer.should_trace(canonical) + if ext_disp: + ext_disp.extension = tracer + return ext_disp + # If the user specified source or include, then that's authoritative # about the outer bound of what to measure and we don't have to apply # any canned exclusions. If they didn't, then we have to exclude the # stdlib and coverage.py directories. if self.source_match: if not self.source_match.match(canonical): - return None, "falls outside the --source trees" + return disp.nope("falls outside the --source trees") elif self.include_match: if not self.include_match.match(canonical): - return None, "falls outside the --include trees" + return disp.nope("falls outside the --include trees") else: # If we aren't supposed to trace installed code, then check if this # is near the Python standard library and skip it if so. if self.pylib_match and self.pylib_match.match(canonical): - return None, "is in the stdlib" + return disp.nope("is in the stdlib") # We exclude the coverage code itself, since a little of it will be # measured otherwise. if self.cover_match and self.cover_match.match(canonical): - return None, "is part of coverage.py" + return disp.nope("is part of coverage.py") # Check the file against the omit pattern. if self.omit_match and self.omit_match.match(canonical): - return None, "is inside an --omit pattern" + return disp.nope("is inside an --omit pattern") - return canonical, "because we love you" + disp.filename = canonical + return disp def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. - Calls `_should_trace_with_reason`, and returns just the decision. + Calls `_should_trace_with_reason`, and returns the FileDisposition. """ - canonical, reason = self._should_trace_with_reason(filename, frame) + disp = self._should_trace_with_reason(filename, frame) if self.debug.should('trace'): - if not canonical: - msg = "Not tracing %r: %s" % (filename, reason) - else: - msg = "Tracing %r" % (filename,) - self.debug.write(msg) - return canonical + self.debug.write(disp.debug_message()) + return disp def _warn(self, msg): """Use `msg` as a warning.""" @@ -525,8 +542,10 @@ class coverage(object): if not self._measured: return + # TODO: seems like this parallel structure is getting kinda old... self.data.add_line_data(self.collector.get_line_data()) self.data.add_arc_data(self.collector.get_arc_data()) + self.data.add_extension_data(self.collector.get_extension_data()) self.collector.reset() # If there are still entries in the source_pkgs list, then we never @@ -594,7 +613,8 @@ class coverage(object): """ self._harvest_data() if not isinstance(it, CodeUnit): - it = code_unit_factory(it, self.file_locator)[0] + get_ext = self.data.extension_data().get + it = code_unit_factory(it, self.file_locator, get_ext)[0] return Analysis(self, it) @@ -760,6 +780,28 @@ class coverage(object): return info +class FileDisposition(object): + """A simple object for noting a number of details of files to trace.""" + def __init__(self, original_filename): + self.original_filename = original_filename + self.filename = None + self.reason = "" + self.extension = None + + def nope(self, reason): + """A helper for returning a NO answer from should_trace.""" + self.reason = reason + return self + + def debug_message(self): + """Produce a debugging message explaining the outcome.""" + if not self.filename: + msg = "Not tracing %r: %s" % (self.original_filename, self.reason) + else: + msg = "Tracing %r" % (self.original_filename,) + return msg + + def process_startup(): """Call this at Python startup to perhaps measure coverage. diff --git a/coverage/data.py b/coverage/data.py index 042b6405..b78c931d 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -21,6 +21,11 @@ class CoverageData(object): * arcs: a dict mapping filenames to sorted lists of line number pairs: { 'file1': [(17,23), (17,25), (25,26)], ... } + * extensions: a dict mapping filenames to extension names: + { 'file1': "django.coverage", ... } + # TODO: how to handle the difference between a extension module + # name, and the class in the module? + """ def __init__(self, basename=None, collector=None, debug=None): @@ -64,6 +69,14 @@ class CoverageData(object): # self.arcs = {} + # A map from canonical source file name to an extension module name: + # + # { + # 'filename1.py': 'django.coverage', + # ... + # } + self.extensions = {} + def usefile(self, use_file=True): """Set whether or not to use a disk file for data.""" self.use_file = use_file @@ -110,6 +123,9 @@ class CoverageData(object): (f, sorted(amap.keys())) for f, amap in iitems(self.arcs) ) + def extension_data(self): + return self.extensions + def write_file(self, filename): """Write the coverage data to `filename`.""" @@ -213,6 +229,9 @@ class CoverageData(object): for filename, arcs in iitems(arc_data): self.arcs.setdefault(filename, {}).update(arcs) + def add_extension_data(self, extension_data): + self.extensions.update(extension_data) + def touch_file(self, filename): """Ensure that `filename` appears in the data, empty if needed.""" self.lines.setdefault(filename, {}) diff --git a/coverage/django.py b/coverage/django.py new file mode 100644 index 00000000..00f2ed54 --- /dev/null +++ b/coverage/django.py @@ -0,0 +1,61 @@ +import sys + + +ALL_TEMPLATE_MAP = {} + +def get_line_map(filename): + if filename not in ALL_TEMPLATE_MAP: + with open(filename) as template_file: + template_source = template_file.read() + line_lengths = [len(l) for l in template_source.splitlines(True)] + ALL_TEMPLATE_MAP[filename] = list(running_sum(line_lengths)) + return ALL_TEMPLATE_MAP[filename] + +def get_line_number(line_map, offset): + for lineno, line_offset in enumerate(line_map, start=1): + if line_offset >= offset: + return lineno + return -1 + +class DjangoTracer(object): + def should_trace(self, canonical): + return "/django/template/" in canonical + + def source(self, frame): + if frame.f_code.co_name != 'render': + return None + that = frame.f_locals['self'] + return getattr(that, "source", None) + + def file_name(self, frame): + source = self.source(frame) + if not source: + return None + return source[0].name.encode(sys.getfilesystemencoding()) + + def line_number_range(self, frame): + source = self.source(frame) + if not source: + return -1, -1 + filename = source[0].name + line_map = get_line_map(filename) + start = get_line_number(line_map, source[1][0]) + end = get_line_number(line_map, source[1][1]) + if start < 0 or end < 0: + return -1, -1 + return start, end + +def running_sum(seq): + total = 0 + for num in seq: + total += num + yield total + +def ppp(obj): + ret = [] + import inspect + for name, value in inspect.getmembers(obj): + if not callable(value): + ret.append("%s=%r" % (name, value)) + attrs = ", ".join(ret) + return "%s: %s" % (obj.__class__, attrs) diff --git a/coverage/execfile.py b/coverage/execfile.py index 10c7b917..b7877b6a 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -1,23 +1,83 @@ """Execute files of Python code.""" -import imp, marshal, os, sys +import marshal, os, sys, types -from coverage.backward import open_python_source +from coverage.backward import open_python_source, BUILTINS +from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec from coverage.misc import ExceptionDuringRun, NoCode, NoSource -try: - # In Py 2.x, the builtins were in __builtin__ - BUILTINS = sys.modules['__builtin__'] -except KeyError: - # In Py 3.x, they're in builtins - BUILTINS = sys.modules['builtins'] +if importlib_util_find_spec: + def find_module(modulename): + """Find the module named `modulename`. + Returns the file path of the module, and the name of the enclosing + package. + """ + # pylint: disable=no-member + try: + spec = importlib_util_find_spec(modulename) + except ImportError as err: + raise NoSource(str(err)) + if not spec: + raise NoSource("No module named %r" % (modulename,)) + pathname = spec.origin + packagename = spec.name + if pathname.endswith("__init__.py"): + mod_main = modulename + ".__main__" + spec = importlib_util_find_spec(mod_main) + if not spec: + raise NoSource( + "No module named %s; " + "%r is a package and cannot be directly executed" + % (mod_main, modulename) + ) + pathname = spec.origin + packagename = spec.name + packagename = packagename.rpartition(".")[0] + return pathname, packagename +else: + def find_module(modulename): + """Find the module named `modulename`. + + Returns the file path of the module, and the name of the enclosing + package. + """ + openfile = None + glo, loc = globals(), locals() + try: + # Search for the module - inside its parent package, if any - using + # standard import mechanics. + if '.' in modulename: + packagename, name = modulename.rsplit('.', 1) + package = __import__(packagename, glo, loc, ['__path__']) + searchpath = package.__path__ + else: + packagename, name = None, modulename + searchpath = None # "top-level search" in imp.find_module() + openfile, pathname, _ = imp.find_module(name, searchpath) -def rsplit1(s, sep): - """The same as s.rsplit(sep, 1), but works in 2.3""" - parts = s.split(sep) - return sep.join(parts[:-1]), parts[-1] + # Complain if this is a magic non-file module. + if openfile is None and pathname is None: + raise NoSource( + "module does not live in a file: %r" % modulename + ) + + # If `modulename` is actually a package, not a mere module, then we + # pretend to be Python 2.7 and try running its __main__.py script. + if openfile is None: + packagename = modulename + name = '__main__' + package = __import__(packagename, glo, loc, ['__path__']) + searchpath = package.__path__ + openfile, pathname, _ = imp.find_module(name, searchpath) + except ImportError as err: + raise NoSource(str(err)) + finally: + if openfile: + openfile.close() + + return pathname, packagename def run_python_module(modulename, args): @@ -28,41 +88,8 @@ def run_python_module(modulename, args): element naming the module being executed. """ - openfile = None - glo, loc = globals(), locals() - try: - # Search for the module - inside its parent package, if any - using - # standard import mechanics. - if '.' in modulename: - packagename, name = rsplit1(modulename, '.') - package = __import__(packagename, glo, loc, ['__path__']) - searchpath = package.__path__ - else: - packagename, name = None, modulename - searchpath = None # "top-level search" in imp.find_module() - openfile, pathname, _ = imp.find_module(name, searchpath) - - # Complain if this is a magic non-file module. - if openfile is None and pathname is None: - raise NoSource( - "module does not live in a file: %r" % modulename - ) + pathname, packagename = find_module(modulename) - # If `modulename` is actually a package, not a mere module, then we - # pretend to be Python 2.7 and try running its __main__.py script. - if openfile is None: - packagename = modulename - name = '__main__' - package = __import__(packagename, glo, loc, ['__path__']) - searchpath = package.__path__ - openfile, pathname, _ = imp.find_module(name, searchpath) - except ImportError as err: - raise NoSource(str(err)) - finally: - if openfile: - openfile.close() - - # Finally, hand the file off to run_python_file for execution. pathname = os.path.abspath(pathname) args[0] = pathname run_python_file(pathname, args, package=packagename) @@ -79,7 +106,7 @@ def run_python_file(filename, args, package=None): """ # Create a module to serve as __main__ old_main_mod = sys.modules['__main__'] - main_mod = imp.new_module('__main__') + main_mod = types.ModuleType('__main__') sys.modules['__main__'] = main_mod main_mod.__file__ = filename if package: @@ -119,6 +146,7 @@ def run_python_file(filename, args, package=None): # Restore the old argv and path sys.argv = old_argv + def make_code_from_py(filename): """Get source from `filename` and make a code object of it.""" # Open the source file. @@ -150,7 +178,7 @@ def make_code_from_pyc(filename): # First four bytes are a version-specific magic number. It has to # match or we won't run the file. magic = fpyc.read(4) - if magic != imp.get_magic(): + if magic != PYC_MAGIC_NUMBER: raise NoCode("Bad magic number in .pyc file") # Skip the junk in the header that we don't need. diff --git a/coverage/extension.py b/coverage/extension.py new file mode 100644 index 00000000..8c89b88e --- /dev/null +++ b/coverage/extension.py @@ -0,0 +1,20 @@ +"""Extension management for coverage.py""" + +def load_extensions(modules, name): + """Load extensions from `modules`, finding them by `name`. + + Yields the loaded extensions. + + """ + + for module in modules: + try: + __import__(module) + mod = sys.modules[module] + except ImportError: + blah() + continue + + entry = getattr(mod, name, None) + if entry: + yield entry diff --git a/coverage/files.py b/coverage/files.py index 94388f96..08ce1e84 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -1,7 +1,7 @@ """File wrangling.""" from coverage.backward import to_string -from coverage.misc import CoverageException +from coverage.misc import CoverageException, join_regex import fnmatch, os, os.path, re, sys import ntpath, posixpath @@ -177,6 +177,7 @@ class FnmatchMatcher(object): """A matcher for files by filename pattern.""" def __init__(self, pats): self.pats = pats[:] + self.re = re.compile(join_regex([fnmatch.translate(p) for p in pats])) def __repr__(self): return "<FnmatchMatcher %r>" % self.pats @@ -187,10 +188,7 @@ class FnmatchMatcher(object): def match(self, fpath): """Does `fpath` match one of our filename patterns?""" - for pat in self.pats: - if fnmatch.fnmatch(fpath, pat): - return True - return False + return self.re.match(fpath) is not None def sep(s): diff --git a/coverage/html.py b/coverage/html.py index 42da2972..6e21efaa 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -149,9 +149,7 @@ class HtmlReporter(Reporter): def html_file(self, cu, analysis): """Generate an HTML file for one source file.""" - source_file = cu.source_file() - with source_file: - source = source_file.read() + source = cu.source() # Find out if the file on disk is already correct. flat_rootname = cu.flat_rootname() @@ -241,7 +239,9 @@ class HtmlReporter(Reporter): })) if sys.version_info < (3, 0): - html = html.decode(encoding) + # In theory, all the characters in the source can be decoded, but + # strange things happen, so use 'replace' to keep errors at bay. + html = html.decode(encoding, 'replace') html_filename = flat_rootname + ".html" html_path = os.path.join(self.directory, html_filename) diff --git a/coverage/misc.py b/coverage/misc.py index c88d4ecd..4b1dccb2 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -87,7 +87,7 @@ def bool_or_none(b): def join_regex(regexes): """Combine a list of regexes into one that matches any of them.""" if len(regexes) > 1: - return "|".join("(%s)" % r for r in regexes) + return "|".join("(?:%s)" % r for r in regexes) elif regexes: return regexes[0] else: diff --git a/coverage/parser.py b/coverage/parser.py index cfaf02fa..c5e95baa 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -14,23 +14,11 @@ class CodeParser(object): """ Base class for any code parser. """ - def _adjust_filename(self, fname): - return fname - - def first_lines(self, lines): - """Map the line numbers in `lines` to the correct first line of the - statement. - - Returns a set of the first lines. - - """ - return set(self.first_line(l) for l in lines) - - def first_line(self, line): - return line - def translate_lines(self, lines): - return lines + return set(lines) + + def translate_arcs(self, arcs): + return arcs def exit_counts(self): return {} @@ -42,7 +30,7 @@ class CodeParser(object): class PythonParser(CodeParser): """Parse code to find executable lines, excluded lines, etc.""" - def __init__(self, cu, text=None, filename=None, exclude=None): + def __init__(self, text=None, filename=None, exclude=None): """ Source can be provided as `text`, the text itself, or `filename`, from which the text will be read. Excluded lines are those that match @@ -197,6 +185,24 @@ class PythonParser(CodeParser): else: return line + def first_lines(self, lines): + """Map the line numbers in `lines` to the correct first line of the + statement. + + Returns a set of the first lines. + + """ + return set(self.first_line(l) for l in lines) + + def translate_lines(self, lines): + return self.first_lines(lines) + + def translate_arcs(self, arcs): + return [ + (self.first_line(a), self.first_line(b)) + for (a, b) in arcs + ] + def parse_source(self): """Parse source text to find executable lines, excluded lines, etc. diff --git a/coverage/phystokens.py b/coverage/phystokens.py index e79ce01f..867388f7 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -120,7 +120,7 @@ def source_encoding(source): # This is mostly code adapted from Py3.2's tokenize module. - cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)") + cookie_re = re.compile(r"^\s*#.*coding[:=]\s*([-\w.]+)") # Do this so the detect_encode code we copied will work. readline = iter(source.splitlines(True)).next diff --git a/coverage/report.py b/coverage/report.py index 34f44422..7627d1aa 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -1,8 +1,8 @@ """Reporter foundation for Coverage.""" -import fnmatch, os +import os from coverage.codeunit import code_unit_factory -from coverage.files import prep_patterns +from coverage.files import prep_patterns, FnmatchMatcher from coverage.misc import CoverageException, NoSource, NotPython class Reporter(object): @@ -33,26 +33,24 @@ class Reporter(object): """ morfs = morfs or self.coverage.data.measured_files() file_locator = self.coverage.file_locator - self.code_units = code_unit_factory(morfs, file_locator) + get_ext = self.coverage.data.extension_data().get + self.code_units = code_unit_factory(morfs, file_locator, get_ext) if self.config.include: patterns = prep_patterns(self.config.include) + matcher = FnmatchMatcher(patterns) filtered = [] for cu in self.code_units: - for pattern in patterns: - if fnmatch.fnmatch(cu.filename, pattern): - filtered.append(cu) - break + if matcher.match(cu.filename): + filtered.append(cu) self.code_units = filtered if self.config.omit: patterns = prep_patterns(self.config.omit) + matcher = FnmatchMatcher(patterns) filtered = [] for cu in self.code_units: - for pattern in patterns: - if fnmatch.fnmatch(cu.filename, pattern): - break - else: + if not matcher.match(cu.filename): filtered.append(cu) self.code_units = filtered diff --git a/coverage/results.py b/coverage/results.py index 08329766..6cbcbfc8 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -14,11 +14,7 @@ class Analysis(object): self.code_unit = code_unit self.filename = self.code_unit.filename - actual_filename, source = self.code_unit.find_source(self.filename) - - self.parser = code_unit.parser_class( - code_unit, - text=source, filename=actual_filename, + self.parser = code_unit.get_parser( exclude=self.coverage._exclude_regex('exclude') ) self.statements, self.excluded = self.parser.parse_source() @@ -26,7 +22,6 @@ class Analysis(object): # Identify missing statements. executed = self.coverage.data.executed_lines(self.filename) executed = self.parser.translate_lines(executed) - executed = self.parser.first_lines(executed) self.missing = self.statements - executed if self.coverage.data.has_arcs(): @@ -74,8 +69,7 @@ class Analysis(object): def arcs_executed(self): """Returns a sorted list of the arcs actually executed in the code.""" executed = self.coverage.data.executed_arcs(self.filename) - m2fl = self.parser.first_line - executed = ((m2fl(l1), m2fl(l2)) for (l1,l2) in executed) + executed = self.parser.translate_arcs(executed) return sorted(executed) def arcs_missing(self): @@ -89,6 +83,23 @@ class Analysis(object): ) return sorted(missing) + def arcs_missing_formatted(self): + """ The missing branch arcs, formatted nicely. + + Returns a string like "1->2, 1->3, 16->20". Omits any mention of + missing lines, so if line 17 is missing, then 16->17 won't be included. + + """ + arcs = self.missing_branch_arcs() + missing = self.missing + line_exits = sorted(iitems(arcs)) + pairs = [] + for line, exits in line_exits: + for ex in sorted(exits): + if line not in missing and ex not in missing: + pairs.append('%d->%d' % (line, ex)) + return ', '.join(pairs) + def arcs_unpredicted(self): """Returns a sorted list of the executed arcs missing from the code.""" possible = self.arc_possibilities() diff --git a/coverage/summary.py b/coverage/summary.py index c99c5303..a6768cf9 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -59,7 +59,14 @@ class SummaryReporter(Reporter): args += (nums.n_branches, nums.n_missing_branches) args += (nums.pc_covered_str,) if self.config.show_missing: - args += (analysis.missing_formatted(),) + missing_fmtd = analysis.missing_formatted() + if self.branches: + branches_fmtd = analysis.arcs_missing_formatted() + if branches_fmtd: + if missing_fmtd: + missing_fmtd += ", " + missing_fmtd += branches_fmtd + args += (missing_fmtd,) outfile.write(fmt_coverage % args) total += nums except KeyboardInterrupt: # pragma: not covered diff --git a/coverage/templite.py b/coverage/templite.py index a71caf63..53824e08 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -15,7 +15,7 @@ class CodeBuilder(object): def __init__(self, indent=0): self.code = [] - self.ident_level = indent + self.indent_level = indent def __str__(self): return "".join(str(c) for c in self.code) @@ -26,28 +26,28 @@ class CodeBuilder(object): Indentation and newline will be added for you, don't provide them. """ - self.code.extend([" " * self.ident_level, line, "\n"]) + self.code.extend([" " * self.indent_level, line, "\n"]) - def add_subbuilder(self): + def add_section(self): """Add a section, a sub-CodeBuilder.""" - sect = CodeBuilder(self.ident_level) - self.code.append(sect) - return sect + section = CodeBuilder(self.indent_level) + self.code.append(section) + return section INDENT_STEP = 4 # PEP8 says so! def indent(self): """Increase the current indent for following lines.""" - self.ident_level += self.INDENT_STEP + self.indent_level += self.INDENT_STEP def dedent(self): """Decrease the current indent for following lines.""" - self.ident_level -= self.INDENT_STEP + self.indent_level -= self.INDENT_STEP def get_globals(self): - """Compile the code, and return a dict of globals it defines.""" + """Execute the code, and return a dict of globals it defines.""" # A check that the caller really finished all the blocks they started. - assert self.ident_level == 0 + assert self.indent_level == 0 # Get the Python source as a single string. python_source = str(self) # Execute the source, defining globals, and return them. @@ -110,21 +110,21 @@ class Templite(object): # it, and execute it to render the template. code = CodeBuilder() - code.add_line("def render_function(ctx, do_dots):") + code.add_line("def render_function(context, do_dots):") code.indent() - vars_code = code.add_subbuilder() + vars_code = code.add_section() code.add_line("result = []") - code.add_line("a = result.append") - code.add_line("e = result.extend") - code.add_line("s = str") + code.add_line("append_result = result.append") + code.add_line("extend_result = result.extend") + code.add_line("to_str = str") buffered = [] def flush_output(): """Force `buffered` to the code builder.""" if len(buffered) == 1: - code.add_line("a(%s)" % buffered[0]) + code.add_line("append_result(%s)" % buffered[0]) elif len(buffered) > 1: - code.add_line("e([%s])" % ", ".join(buffered)) + code.add_line("extend_result([%s])" % ", ".join(buffered)) del buffered[:] ops_stack = [] @@ -138,7 +138,8 @@ class Templite(object): continue elif token.startswith('{{'): # An expression to evaluate. - buffered.append("s(%s)" % self._expr_code(token[2:-2].strip())) + expr = self._expr_code(token[2:-2].strip()) + buffered.append("to_str(%s)" % expr) elif token.startswith('{%'): # Action tag: split into words and parse further. flush_output() @@ -187,7 +188,7 @@ class Templite(object): flush_output() for var_name in self.all_vars - self.loop_vars: - vars_code.add_line("c_%s = ctx[%r]" % (var_name, var_name)) + vars_code.add_line("c_%s = context[%r]" % (var_name, var_name)) code.add_line("return ''.join(result)") code.dedent() @@ -234,10 +235,10 @@ class Templite(object): """ # Make the complete context we'll use. - ctx = dict(self.context) + render_context = dict(self.context) if context: - ctx.update(context) - return self._render_function(ctx, self._do_dots) + render_context.update(context) + return self._render_function(render_context, self._do_dots) def _do_dots(self, value, *dots): """Evaluate dotted expressions at runtime.""" diff --git a/coverage/tracer.c b/coverage/tracer.c index 97dd113b..ca8d61c1 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -259,6 +259,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse int ret = RET_OK; PyObject * filename = NULL; PyObject * tracename = NULL; + PyObject * disposition = NULL; #if WHAT_LOG || TRACE_LOG PyObject * ascii = NULL; #endif @@ -335,41 +336,51 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse /* Check if we should trace this line. */ filename = frame->f_code->co_filename; - tracename = PyDict_GetItem(self->should_trace_cache, filename); - if (tracename == NULL) { + disposition = PyDict_GetItem(self->should_trace_cache, filename); + if (disposition == NULL) { STATS( self->stats.new_files++; ) /* We've never considered this file before. */ /* Ask should_trace about it. */ PyObject * args = Py_BuildValue("(OO)", filename, frame); - tracename = PyObject_Call(self->should_trace, args, NULL); + disposition = PyObject_Call(self->should_trace, args, NULL); Py_DECREF(args); - if (tracename == NULL) { + if (disposition == NULL) { /* An error occurred inside should_trace. */ STATS( self->stats.errors++; ) return RET_ERROR; } - if (PyDict_SetItem(self->should_trace_cache, filename, tracename) < 0) { + if (PyDict_SetItem(self->should_trace_cache, filename, disposition) < 0) { STATS( self->stats.errors++; ) return RET_ERROR; } } else { - Py_INCREF(tracename); + Py_INCREF(disposition); } /* If tracename is a string, then we're supposed to trace. */ + tracename = PyObject_GetAttrString(disposition, "filename"); + if (tracename == NULL) { + STATS( self->stats.errors++; ) + Py_DECREF(disposition); + return RET_ERROR; + } if (MyText_Check(tracename)) { PyObject * file_data = PyDict_GetItem(self->data, tracename); if (file_data == NULL) { file_data = PyDict_New(); if (file_data == NULL) { STATS( self->stats.errors++; ) + Py_DECREF(tracename); + Py_DECREF(disposition); return RET_ERROR; } ret = PyDict_SetItem(self->data, tracename, file_data); Py_DECREF(file_data); if (ret < 0) { STATS( self->stats.errors++; ) + Py_DECREF(tracename); + Py_DECREF(disposition); return RET_ERROR; } } @@ -385,6 +396,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse } Py_DECREF(tracename); + Py_DECREF(disposition); self->last_line = -1; break; diff --git a/doc/config.rst b/doc/config.rst index 7ff82021..882fc777 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -41,7 +41,7 @@ Boolean values can be specified as ``on``, ``off``, ``true``, ``false``, ``1``, or ``0`` and are case-insensitive. Environment variables can be substituted in by using dollar signs: ``$WORD`` -``${WORD}`` will be replaced with the value of ``WORD`` in the environment. +or ``${WORD}`` will be replaced with the value of ``WORD`` in the environment. A dollar sign can be inserted with ``$$``. Missing environment variables will result in empty strings with no error. diff --git a/doc/faq.rst b/doc/faq.rst index 78db591f..d7ae3641 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -50,6 +50,19 @@ If you are using the :ref:`API <api>`, you need to call coverage.start() before importing the modules that define your functions. +**Q: Coverage is much slower than I remember, what's going on?** + +Make sure you are using the C trace function. Coverage.py provides two +implementations of the trace function. The C implementation runs much faster. +To see what you are running, use ``coverage debug sys``. The output contains +details of the environment, including a line that says either ``tracer: CTracer`` +or ``tracer: PyTracer``. If it says ``PyTracer`` then you are using the +slow Python implementation. + +Try re-installing coverage.py to see what happened and if you get the CTracer +as you should. + + **Q: Does coverage.py work on Python 3.x?** Yes, Python 3 is fully supported. @@ -12,9 +12,13 @@ import os import platform import socket import sys +import warnings import zipfile +warnings.simplefilter("default") + + # Functions named do_* are executable from the command line: do_blah is run # by "python igor.py blah". diff --git a/tests/backtest.py b/tests/backtest.py index 89a25536..439493d1 100644 --- a/tests/backtest.py +++ b/tests/backtest.py @@ -4,41 +4,31 @@ # (Redefining built-in blah) # The whole point of this file is to redefine built-ins, so shut up about it. -import os +import subprocess -# Py2 and Py3 don't agree on how to run commands in a subprocess. -try: - import subprocess -except ImportError: - def run_command(cmd, status=0): - """Run a command in a subprocess. - - Returns the exit status code and the combined stdout and stderr. - """ - _, stdouterr = os.popen4(cmd) - return status, stdouterr.read() +# This isn't really a backward compatibility thing, should be moved into a +# helpers file or something. +def run_command(cmd): + """Run a command in a subprocess. -else: - def run_command(cmd, status=0): - """Run a command in a subprocess. + Returns the exit status code and the combined stdout and stderr. - Returns the exit status code and the combined stdout and stderr. + """ + proc = subprocess.Popen(cmd, shell=True, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + output, _ = proc.communicate() + status = proc.returncode # pylint: disable=E1101 - """ - proc = subprocess.Popen(cmd, shell=True, - stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT - ) - output, _ = proc.communicate() - status = proc.returncode # pylint: disable=E1101 + # Get the output, and canonicalize it to strings with newlines. + if not isinstance(output, str): + output = output.decode('utf-8') + output = output.replace('\r', '') - # Get the output, and canonicalize it to strings with newlines. - if not isinstance(output, str): - output = output.decode('utf-8') - output = output.replace('\r', '') + return status, output - return status, output # No more execfile in Py3 try: @@ -46,4 +36,6 @@ try: except NameError: def execfile(filename, globs): """A Python 3 implementation of execfile.""" - exec(compile(open(filename).read(), filename, 'exec'), globs) + with open(filename) as fobj: + code = fobj.read() + exec(compile(code, filename, 'exec'), globs) diff --git a/tests/backunittest.py b/tests/backunittest.py index ca741d37..6498397f 100644 --- a/tests/backunittest.py +++ b/tests/backunittest.py @@ -8,9 +8,9 @@ except ImportError: import unittest -def _need(method): - """Do we need to define our own `method` method?""" - return not hasattr(unittest.TestCase, method) +def unittest_has(method): + """Does `unitttest.TestCase` have `method` defined?""" + return hasattr(unittest.TestCase, method) class TestCase(unittest.TestCase): @@ -20,7 +20,22 @@ class TestCase(unittest.TestCase): `unittest` doesn't have them. """ - if _need('assertSameElements'): - def assertSameElements(self, s1, s2): - """Assert that the two arguments are equal as sets.""" - self.assertEqual(set(s1), set(s2)) + # pylint: disable=missing-docstring + + if not unittest_has('assertCountEqual'): + if unittest_has('assertSameElements'): + def assertCountEqual(self, *args, **kwargs): + # pylint: disable=no-member + return self.assertSameElements(*args, **kwargs) + else: + def assertCountEqual(self, s1, s2): + """Assert these have the same elements, regardless of order.""" + self.assertEqual(set(s1), set(s2)) + + if not unittest_has('assertRaisesRegex'): + def assertRaisesRegex(self, *args, **kwargs): + return self.assertRaisesRegexp(*args, **kwargs) + + if not unittest_has('assertRegex'): + def assertRegex(self, *args, **kwargs): + return self.assertRegexpMatches(*args, **kwargs) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index a309f17d..1eedad39 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -1,10 +1,11 @@ """Base test case class for coverage testing.""" -import glob, imp, os, random, shlex, shutil, sys, tempfile, textwrap +import glob, os, random, re, shlex, shutil, sys, tempfile, textwrap import atexit, collections import coverage -from coverage.backward import StringIO, to_bytes +from coverage.backward import StringIO, to_bytes, import_local_file +from coverage.backward import importlib # pylint: disable=unused-import from coverage.control import _TEST_NAME_FILE from tests.backtest import run_command from tests.backunittest import TestCase @@ -221,18 +222,7 @@ class CoverageTest(TestCase): as `modname`, and returns the module object. """ - modfile = modname + '.py' - - for suff in imp.get_suffixes(): - if suff[0] == '.py': - break - - with open(modfile, 'r') as f: - # pylint: disable=W0631 - # (Using possibly undefined loop variable 'suff') - mod = imp.load_module(modname, f, modfile, suff) - - return mod + return import_local_file(modname) def start_import_stop(self, cov, modname): """Start coverage, import a file, then stop coverage. @@ -365,19 +355,21 @@ class CoverageTest(TestCase): if statements == line_list: break else: - self.fail("None of the lines choices matched %r" % - statements + self.fail( + "None of the lines choices matched %r" % statements ) + missing_formatted = analysis.missing_formatted() if type(missing) == type(""): - self.assertEqual(analysis.missing_formatted(), missing) + self.assertEqual(missing_formatted, missing) else: for missing_list in missing: - if analysis.missing_formatted() == missing_list: + if missing_formatted == missing_list: break else: - self.fail("None of the missing choices matched %r" % - analysis.missing_formatted() + self.fail( + "None of the missing choices matched %r" % + missing_formatted ) if arcs is not None: @@ -412,17 +404,17 @@ class CoverageTest(TestCase): """Assert that `flist1` and `flist2` are the same set of file names.""" flist1_nice = [self.nice_file(f) for f in flist1] flist2_nice = [self.nice_file(f) for f in flist2] - self.assertSameElements(flist1_nice, flist2_nice) + self.assertCountEqual(flist1_nice, flist2_nice) def assert_exists(self, fname): """Assert that `fname` is a file that exists.""" msg = "File %r should exist" % fname - self.assert_(os.path.exists(fname), msg) + self.assertTrue(os.path.exists(fname), msg) def assert_doesnt_exist(self, fname): """Assert that `fname` is a file that doesn't exist.""" msg = "File %r shouldn't exist" % fname - self.assert_(not os.path.exists(fname), msg) + self.assertTrue(not os.path.exists(fname), msg) def assert_starts_with(self, s, prefix, msg=None): """Assert that `s` starts with `prefix`.""" @@ -466,7 +458,7 @@ class CoverageTest(TestCase): _, output = self.run_command_status(cmd) return output - def run_command_status(self, cmd, status=0): + def run_command_status(self, cmd): """Run the command-line `cmd` in a subprocess, and print its output. Use this when you need to test the process behavior of coverage. @@ -475,9 +467,6 @@ class CoverageTest(TestCase): Returns a pair: the process' exit status and stdout text. - The `status` argument is returned as the status on older Pythons where - we can't get the actual exit status of the process. - """ # Add our test modules directory to PYTHONPATH. I'm sure there's too # much path munging here, but... @@ -490,10 +479,35 @@ class CoverageTest(TestCase): pypath += testmods + os.pathsep + zipfile self.set_environ('PYTHONPATH', pypath) - status, output = run_command(cmd, status=status) + status, output = run_command(cmd) print(output) return status, output + def report_from_command(self, cmd): + """Return the report from the `cmd`, with some convenience added.""" + report = self.run_command(cmd).replace('\\', '/') + self.assertNotIn("error", report.lower()) + return report + + def report_lines(self, report): + """Return the lines of the report, as a list.""" + lines = report.split('\n') + self.assertEqual(lines[-1], "") + return lines[:-1] + + def line_count(self, report): + """How many lines are in `report`?""" + return len(self.report_lines(report)) + + def squeezed_lines(self, report): + """Return a list of the lines in report, with the spaces squeezed.""" + lines = self.report_lines(report) + return [re.sub(r"\s+", " ", l.strip()) for l in lines] + + def last_line_squeezed(self, report): + """Return the last line of `report` with the spaces squeezed down.""" + return self.squeezed_lines(report)[-1] + # We run some tests in temporary directories, because they may need to make # files for the tests. But this is expensive, so we can change per-class # whether a temp dir is used or not. It's easy to forget to set that diff --git a/tests/farm/annotate/annotate_dir.py b/tests/farm/annotate/annotate_dir.py index 3e37f9ed..86c18cab 100644 --- a/tests/farm/annotate/annotate_dir.py +++ b/tests/farm/annotate/annotate_dir.py @@ -1,7 +1,7 @@ copy("src", "run") run(""" - coverage -e -x multi.py - coverage -a -d out_anno_dir + coverage run multi.py + coverage annotate -d out_anno_dir """, rundir="run") compare("run/out_anno_dir", "gold_anno_dir", "*,cover", left_extra=True) clean("run") diff --git a/tests/farm/annotate/run.py b/tests/farm/annotate/run.py index c645f21c..236f401f 100644 --- a/tests/farm/annotate/run.py +++ b/tests/farm/annotate/run.py @@ -1,7 +1,7 @@ copy("src", "out") run(""" - coverage -e -x white.py - coverage -a white.py + coverage run white.py + coverage annotate white.py """, rundir="out") compare("out", "gold", "*,cover") clean("out") diff --git a/tests/farm/annotate/run_multi.py b/tests/farm/annotate/run_multi.py index 4e8252ed..ef1e8238 100644 --- a/tests/farm/annotate/run_multi.py +++ b/tests/farm/annotate/run_multi.py @@ -1,7 +1,7 @@ copy("src", "out_multi") run(""" - coverage -e -x multi.py - coverage -a + coverage run multi.py + coverage annotate """, rundir="out_multi") compare("out_multi", "gold_multi", "*,cover") clean("out_multi") diff --git a/tests/farm/run/run_chdir.py b/tests/farm/run/run_chdir.py index f459f500..367cd0ad 100644 --- a/tests/farm/run/run_chdir.py +++ b/tests/farm/run/run_chdir.py @@ -1,7 +1,7 @@ copy("src", "out") run(""" coverage run chdir.py - coverage -r + coverage report """, rundir="out", outfile="stdout.txt") contains("out/stdout.txt", "Line One", diff --git a/tests/farm/run/run_timid.py b/tests/farm/run/run_timid.py index ce78fff1..d4e69a46 100644 --- a/tests/farm/run/run_timid.py +++ b/tests/farm/run/run_timid.py @@ -17,8 +17,8 @@ if os.environ.get('COVERAGE_COVERAGE', ''): copy("src", "out") run(""" python showtrace.py none - coverage -e -x showtrace.py regular - coverage -e -x --timid showtrace.py timid + coverage run showtrace.py regular + coverage run --timid showtrace.py timid """, rundir="out", outfile="showtraceout.txt") # When running without coverage, no trace function @@ -42,8 +42,8 @@ old_opts = os.environ.get('COVERAGE_OPTIONS') os.environ['COVERAGE_OPTIONS'] = '--timid' run(""" - coverage -e -x showtrace.py regular - coverage -e -x --timid showtrace.py timid + coverage run showtrace.py regular + coverage run --timid showtrace.py timid """, rundir="out", outfile="showtraceout.txt") contains("out/showtraceout.txt", diff --git a/tests/farm/run/run_xxx.py b/tests/farm/run/run_xxx.py index 19e94a42..6fedc934 100644 --- a/tests/farm/run/run_xxx.py +++ b/tests/farm/run/run_xxx.py @@ -1,7 +1,7 @@ copy("src", "out") run(""" - coverage -e -x xxx - coverage -r + coverage run xxx + coverage report """, rundir="out", outfile="stdout.txt") contains("out/stdout.txt", "xxx: 3 4 0 7", diff --git a/tests/modules/pkg1/p1a.py b/tests/modules/pkg1/p1a.py index be5fcdd3..337add49 100644 --- a/tests/modules/pkg1/p1a.py +++ b/tests/modules/pkg1/p1a.py @@ -1,5 +1,5 @@ import os, sys # Invoke functions in os and sys so we can see if we measure code there. -x = sys.getcheckinterval() +x = sys.getfilesystemencoding() y = os.getcwd() diff --git a/tests/test_api.py b/tests/test_api.py index 097947d2..31bfc57f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -100,7 +100,7 @@ class ApiTest(CoverageTest): """Assert that the files here are `files`, ignoring the usual junk.""" here = os.listdir(".") here = self.clean_files(here, ["*.pyc", "__pycache__"]) - self.assertSameElements(here, files) + self.assertCountEqual(here, files) def test_unexecuted_file(self): cov = coverage.coverage() @@ -221,7 +221,7 @@ class ApiTest(CoverageTest): self.assertEqual(cov.get_exclude_list(), ["foo"]) cov.exclude("bar") self.assertEqual(cov.get_exclude_list(), ["foo", "bar"]) - self.assertEqual(cov._exclude_regex('exclude'), "(foo)|(bar)") + self.assertEqual(cov._exclude_regex('exclude'), "(?:foo)|(?:bar)") cov.clear_exclude() self.assertEqual(cov.get_exclude_list(), []) @@ -233,7 +233,9 @@ class ApiTest(CoverageTest): self.assertEqual(cov.get_exclude_list(which='partial'), ["foo"]) cov.exclude("bar", which='partial') self.assertEqual(cov.get_exclude_list(which='partial'), ["foo", "bar"]) - self.assertEqual(cov._exclude_regex(which='partial'), "(foo)|(bar)") + self.assertEqual( + cov._exclude_regex(which='partial'), "(?:foo)|(?:bar)" + ) cov.clear_exclude(which='partial') self.assertEqual(cov.get_exclude_list(which='partial'), []) diff --git a/tests/test_backward.py b/tests/test_backward.py index e98017ae..2c688edd 100644 --- a/tests/test_backward.py +++ b/tests/test_backward.py @@ -12,7 +12,7 @@ class BackwardTest(TestCase): def test_iitems(self): d = {'a': 1, 'b': 2, 'c': 3} items = [('a', 1), ('b', 2), ('c', 3)] - self.assertSameElements(list(iitems(d)), items) + self.assertCountEqual(list(iitems(d)), items) def test_binary_bytes(self): byte_values = [0, 255, 17, 23, 42, 57] diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 99bae516..038e9214 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -754,7 +754,7 @@ class CmdMainTest(CoverageTest): self.assertEqual(err[-2], 'Exception: oh noes!') def test_internalraise(self): - with self.assertRaisesRegexp(ValueError, "coverage is broken"): + with self.assertRaisesRegex(ValueError, "coverage is broken"): coverage.cmdline.main(['internalraise']) def test_exit(self): diff --git a/tests/test_codeunit.py b/tests/test_codeunit.py index e4912e11..fe82ea1c 100644 --- a/tests/test_codeunit.py +++ b/tests/test_codeunit.py @@ -31,9 +31,9 @@ class CodeUnitTest(CoverageTest): self.assertEqual(acu[0].flat_rootname(), "aa_afile") self.assertEqual(bcu[0].flat_rootname(), "aa_bb_bfile") self.assertEqual(ccu[0].flat_rootname(), "aa_bb_cc_cfile") - self.assertEqual(acu[0].source_file().read(), "# afile.py\n") - self.assertEqual(bcu[0].source_file().read(), "# bfile.py\n") - self.assertEqual(ccu[0].source_file().read(), "# cfile.py\n") + self.assertEqual(acu[0].source(), "# afile.py\n") + self.assertEqual(bcu[0].source(), "# bfile.py\n") + self.assertEqual(ccu[0].source(), "# cfile.py\n") def test_odd_filenames(self): acu = code_unit_factory("aa/afile.odd.py", FileLocator()) @@ -45,9 +45,9 @@ class CodeUnitTest(CoverageTest): self.assertEqual(acu[0].flat_rootname(), "aa_afile_odd") self.assertEqual(bcu[0].flat_rootname(), "aa_bb_bfile_odd") self.assertEqual(b2cu[0].flat_rootname(), "aa_bb_odd_bfile") - self.assertEqual(acu[0].source_file().read(), "# afile.odd.py\n") - self.assertEqual(bcu[0].source_file().read(), "# bfile.odd.py\n") - self.assertEqual(b2cu[0].source_file().read(), "# bfile.py\n") + self.assertEqual(acu[0].source(), "# afile.odd.py\n") + self.assertEqual(bcu[0].source(), "# bfile.odd.py\n") + self.assertEqual(b2cu[0].source(), "# bfile.py\n") def test_modules(self): import aa, aa.bb, aa.bb.cc @@ -58,9 +58,9 @@ class CodeUnitTest(CoverageTest): self.assertEqual(cu[0].flat_rootname(), "aa") self.assertEqual(cu[1].flat_rootname(), "aa_bb") self.assertEqual(cu[2].flat_rootname(), "aa_bb_cc") - self.assertEqual(cu[0].source_file().read(), "# aa\n") - self.assertEqual(cu[1].source_file().read(), "# bb\n") - self.assertEqual(cu[2].source_file().read(), "") # yes, empty + self.assertEqual(cu[0].source(), "# aa\n") + self.assertEqual(cu[1].source(), "# bb\n") + self.assertEqual(cu[2].source(), "") # yes, empty def test_module_files(self): import aa.afile, aa.bb.bfile, aa.bb.cc.cfile @@ -72,9 +72,9 @@ class CodeUnitTest(CoverageTest): self.assertEqual(cu[0].flat_rootname(), "aa_afile") self.assertEqual(cu[1].flat_rootname(), "aa_bb_bfile") self.assertEqual(cu[2].flat_rootname(), "aa_bb_cc_cfile") - self.assertEqual(cu[0].source_file().read(), "# afile.py\n") - self.assertEqual(cu[1].source_file().read(), "# bfile.py\n") - self.assertEqual(cu[2].source_file().read(), "# cfile.py\n") + self.assertEqual(cu[0].source(), "# afile.py\n") + self.assertEqual(cu[1].source(), "# bfile.py\n") + self.assertEqual(cu[2].source(), "# cfile.py\n") def test_comparison(self): acu = code_unit_factory("aa/afile.py", FileLocator())[0] @@ -97,7 +97,7 @@ class CodeUnitTest(CoverageTest): self.assert_doesnt_exist(egg1.__file__) cu = code_unit_factory([egg1, egg1.egg1], FileLocator()) - self.assertEqual(cu[0].source_file().read(), "") - self.assertEqual(cu[1].source_file().read().split("\n")[0], + self.assertEqual(cu[0].source(), "") + self.assertEqual(cu[1].source().split("\n")[0], "# My egg file!" ) diff --git a/tests/test_config.py b/tests/test_config.py index 7fa31208..7409f4aa 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -125,58 +125,71 @@ class ConfigTest(CoverageTest): class ConfigFileTest(CoverageTest): """Tests of the config file settings in particular.""" - def test_config_file_settings(self): - # This sample file tries to use lots of variation of syntax... - self.make_file(".coveragerc", """\ - # This is a settings file for coverage.py - [run] - timid = yes - data_file = something_or_other.dat - branch = 1 - cover_pylib = TRUE - parallel = on - include = a/ , b/ - - [report] - ; these settings affect reporting. - exclude_lines = - if 0: - - pragma:?\\s+no cover - another_tab - - ignore_errors = TRUE - omit = - one, another, some_more, - yet_more - precision = 3 - - partial_branches = - pragma:?\\s+no branch - partial_branches_always = - if 0: - while True: - - show_missing= TruE - - [html] - - directory = c:\\tricky\\dir.somewhere - extra_css=something/extra.css - title = Title & nums # nums! - [xml] - output=mycov.xml - - [paths] - source = - . - /home/ned/src/ - - other = other, /home/ned/other, c:\\Ned\\etc - - """) - cov = coverage.coverage() - + # This sample file tries to use lots of variation of syntax... + # The {section} placeholder lets us nest these settings in another file. + LOTSA_SETTINGS = """\ + # This is a settings file for coverage.py + [{section}run] + timid = yes + data_file = something_or_other.dat + branch = 1 + cover_pylib = TRUE + parallel = on + include = a/ , b/ + + [{section}report] + ; these settings affect reporting. + exclude_lines = + if 0: + + pragma:?\\s+no cover + another_tab + + ignore_errors = TRUE + omit = + one, another, some_more, + yet_more + precision = 3 + + partial_branches = + pragma:?\\s+no branch + partial_branches_always = + if 0: + while True: + + show_missing= TruE + + [{section}html] + + directory = c:\\tricky\\dir.somewhere + extra_css=something/extra.css + title = Title & nums # nums! + [{section}xml] + output=mycov.xml + + [{section}paths] + source = + . + /home/ned/src/ + + other = other, /home/ned/other, c:\\Ned\\etc + + """ + + # Just some sample setup.cfg text from the docs. + SETUP_CFG = """\ + [bdist_rpm] + release = 1 + packager = Jane Packager <janep@pysoft.com> + doc_files = CHANGES.txt + README.txt + USAGE.txt + doc/ + examples/ + """ + + def assert_config_settings_are_correct(self, cov): + """Check that `cov` has all the settings from LOTSA_SETTINGS.""" self.assertTrue(cov.config.timid) self.assertEqual(cov.config.data_file, "something_or_other.dat") self.assertTrue(cov.config.branch) @@ -211,8 +224,33 @@ class ConfigFileTest(CoverageTest): 'other': ['other', '/home/ned/other', 'c:\\Ned\\etc'] }) + def test_config_file_settings(self): + self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) + cov = coverage.coverage() + self.assert_config_settings_are_correct(cov) + + def test_config_file_settings_in_setupcfg(self): + nested = self.LOTSA_SETTINGS.format(section="coverage:") + self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG) + cov = coverage.coverage() + self.assert_config_settings_are_correct(cov) + + def test_setupcfg_only_if_not_coveragerc(self): + self.make_file(".coveragerc", """\ + [run] + include = foo + """) + self.make_file("setup.cfg", """\ + [run] + omit = bar + branch = true + """) + cov = coverage.coverage() + self.assertEqual(cov.config.include, ["foo"]) + self.assertEqual(cov.config.omit, None) + self.assertEqual(cov.config.branch, False) + def test_one(self): - # This sample file tries to use lots of variation of syntax... self.make_file(".coveragerc", """\ [html] title = tabblo & «ταБЬℓσ» # numbers diff --git a/tests/test_coroutine.py b/tests/test_coroutine.py index 28539801..fe6c8326 100644 --- a/tests/test_coroutine.py +++ b/tests/test_coroutine.py @@ -1,5 +1,7 @@ """Tests for coroutining.""" +import os.path, sys + from nose.plugins.skip import SkipTest import coverage @@ -20,15 +22,19 @@ except ImportError: def line_count(s): - """How many non-blank lines are in `s`?""" - return sum(1 for l in s.splitlines() if l.strip()) + """How many non-blank non-comment lines are in `s`?""" + def code_line(l): + """Is this a code line? Not blank, and not a full-line comment.""" + return l.strip() and not l.strip().startswith('#') + return sum(1 for l in s.splitlines() if code_line(l)) class CoroutineTest(CoverageTest): """Tests of the coroutine support in coverage.py.""" - # The code common to all the concurrency models. Don't use any comments, - # we're counting non-blank lines to see they are all covered. + LIMIT = 1000 + + # The code common to all the concurrency models. COMMON = """ class Producer(threading.Thread): def __init__(self, q): @@ -36,7 +42,7 @@ class CoroutineTest(CoverageTest): self.q = q def run(self): - for i in range(1000): + for i in range({LIMIT}): self.q.put(i) self.q.put(None) @@ -54,28 +60,32 @@ class CoroutineTest(CoverageTest): break sum += i - q = Queue.Queue() + q = queue.Queue() c = Consumer(q) - c.start() p = Producer(q) + c.start() p.start() + p.join() c.join() - """ + """.format(LIMIT=LIMIT) # Import the things to use threads. - THREAD = """\ + if sys.version_info < (3, 0): + THREAD = """\ + import threading + import Queue as queue + """ + COMMON + else: + THREAD = """\ import threading - try: - import Queue - except ImportError: # Python 3 :) - import queue as Queue + import queue """ + COMMON # Import the things to use eventlet. EVENTLET = """\ import eventlet.green.threading as threading - import eventlet.queue as Queue + import eventlet.queue as queue """ + COMMON # Import the things to use gevent. @@ -83,7 +93,7 @@ class CoroutineTest(CoverageTest): from gevent import monkey monkey.patch_thread() import threading - import gevent.queue as Queue + import gevent.queue as queue """ + COMMON def try_some_code(self, code, args): @@ -91,16 +101,21 @@ class CoroutineTest(CoverageTest): self.make_file("try_it.py", code) - raise SkipTest("Need to put this on a back burner for a while...") - - out = self.run_command("coverage run %s try_it.py" % args) - expected_out = "%d\n" % (sum(range(1000))) + out = self.run_command("coverage run --timid %s try_it.py" % args) + expected_out = "%d\n" % (sum(range(self.LIMIT))) self.assertEqual(out, expected_out) # Read the coverage file and see that try_it.py has all its lines # executed. data = coverage.CoverageData() data.read_file(".coverage") + + # If the test fails, it's helpful to see this info: + fname = os.path.abspath("try_it.py") + linenos = data.executed_lines(fname).keys() + print("{0}: {1}".format(len(linenos), linenos)) + print_simple_annotation(code, linenos) + lines = line_count(code) self.assertEqual(data.summary()['try_it.py'], lines) @@ -114,7 +129,15 @@ class CoroutineTest(CoverageTest): self.try_some_code(self.EVENTLET, "--coroutine=eventlet") def test_gevent(self): + raise SkipTest("Still not sure why gevent isn't working...") + if gevent is None: raise SkipTest("No gevent available") self.try_some_code(self.GEVENT, "--coroutine=gevent") + + +def print_simple_annotation(code, linenos): + """Print the lines in `code` with X for each line number in `linenos`.""" + for lineno, line in enumerate(code.splitlines(), start=1): + print(" {0:s} {1}".format("X" if lineno in linenos else " ", line)) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 33f644fa..565fa4e1 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -46,7 +46,7 @@ class TestCoverageTest(CoverageTest): def test_failed_coverage(self): # If the lines are wrong, the message shows right and wrong. - with self.assertRaisesRegexp(AssertionError, r"\[1, 2] != \[1]"): + with self.assertRaisesRegex(AssertionError, r"\[1, 2] != \[1]"): self.check_coverage("""\ a = 1 b = 2 @@ -55,7 +55,7 @@ class TestCoverageTest(CoverageTest): ) # If the list of lines possibilities is wrong, the msg shows right. msg = r"None of the lines choices matched \[1, 2]" - with self.assertRaisesRegexp(AssertionError, msg): + with self.assertRaisesRegex(AssertionError, msg): self.check_coverage("""\ a = 1 b = 2 @@ -63,7 +63,7 @@ class TestCoverageTest(CoverageTest): ([1], [2]) ) # If the missing lines are wrong, the message shows right and wrong. - with self.assertRaisesRegexp(AssertionError, r"'3' != '37'"): + with self.assertRaisesRegex(AssertionError, r"'3' != '37'"): self.check_coverage("""\ a = 1 if a == 2: @@ -74,7 +74,7 @@ class TestCoverageTest(CoverageTest): ) # If the missing lines possibilities are wrong, the msg shows right. msg = r"None of the missing choices matched '3'" - with self.assertRaisesRegexp(AssertionError, msg): + with self.assertRaisesRegex(AssertionError, msg): self.check_coverage("""\ a = 1 if a == 2: @@ -1671,7 +1671,7 @@ class ReportingTest(CoverageTest): def test_no_data_to_report_on_annotate(self): # Reporting with no data produces a nice message and no output dir. - with self.assertRaisesRegexp(CoverageException, "No data to report."): + with self.assertRaisesRegex(CoverageException, "No data to report."): self.command_line("annotate -d ann") self.assert_doesnt_exist("ann") @@ -1681,12 +1681,12 @@ class ReportingTest(CoverageTest): def test_no_data_to_report_on_html(self): # Reporting with no data produces a nice message and no output dir. - with self.assertRaisesRegexp(CoverageException, "No data to report."): + with self.assertRaisesRegex(CoverageException, "No data to report."): self.command_line("html -d htmlcov") self.assert_doesnt_exist("htmlcov") def test_no_data_to_report_on_xml(self): # Reporting with no data produces a nice message. - with self.assertRaisesRegexp(CoverageException, "No data to report."): + with self.assertRaisesRegex(CoverageException, "No data to report."): self.command_line("xml") self.assert_doesnt_exist("coverage.xml") diff --git a/tests/test_data.py b/tests/test_data.py index 31578f26..b048fd18 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -33,7 +33,7 @@ class DataTest(CoverageTest): def assert_measured_files(self, covdata, measured): """Check that `covdata`'s measured files are `measured`.""" - self.assertSameElements(covdata.measured_files(), measured) + self.assertCountEqual(covdata.measured_files(), measured) def test_reading_empty(self): covdata = CoverageData() @@ -96,9 +96,9 @@ class DataTest(CoverageTest): data = pickle.load(fdata) lines = data['lines'] - self.assertSameElements(lines.keys(), MEASURED_FILES_1) - self.assertSameElements(lines['a.py'], A_PY_LINES_1) - self.assertSameElements(lines['b.py'], B_PY_LINES_1) + self.assertCountEqual(lines.keys(), MEASURED_FILES_1) + self.assertCountEqual(lines['a.py'], A_PY_LINES_1) + self.assertCountEqual(lines['b.py'], B_PY_LINES_1) # If not measuring branches, there's no arcs entry. self.assertEqual(data.get('arcs', 'not there'), 'not there') @@ -111,10 +111,10 @@ class DataTest(CoverageTest): with open(".coverage", 'rb') as fdata: data = pickle.load(fdata) - self.assertSameElements(data['lines'].keys(), []) + self.assertCountEqual(data['lines'].keys(), []) arcs = data['arcs'] - self.assertSameElements(arcs['x.py'], X_PY_ARCS_3) - self.assertSameElements(arcs['y.py'], Y_PY_ARCS_3) + self.assertCountEqual(arcs['x.py'], X_PY_ARCS_3) + self.assertCountEqual(arcs['y.py'], Y_PY_ARCS_3) def test_combining_with_aliases(self): covdata1 = CoverageData() diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 7cd8ac4e..2427847e 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -118,11 +118,11 @@ class RunPycFileTest(CoverageTest): fpyc.write(binary_bytes([0x2a, 0xeb, 0x0d, 0x0a])) fpyc.close() - with self.assertRaisesRegexp(NoCode, "Bad magic number in .pyc file"): + with self.assertRaisesRegex(NoCode, "Bad magic number in .pyc file"): run_python_file(pycfile, [pycfile]) def test_no_such_pyc_file(self): - with self.assertRaisesRegexp(NoCode, "No file to run: 'xyzzy.pyc'"): + with self.assertRaisesRegex(NoCode, "No file to run: 'xyzzy.pyc'"): run_python_file("xyzzy.pyc", []) @@ -138,22 +138,27 @@ class RunModuleTest(CoverageTest): def test_runmod1(self): run_python_module("runmod1", ["runmod1", "hello"]) + self.assertEqual(self.stderr(), "") self.assertEqual(self.stdout(), "runmod1: passed hello\n") def test_runmod2(self): run_python_module("pkg1.runmod2", ["runmod2", "hello"]) + self.assertEqual(self.stderr(), "") self.assertEqual(self.stdout(), "runmod2: passed hello\n") def test_runmod3(self): run_python_module("pkg1.sub.runmod3", ["runmod3", "hello"]) + self.assertEqual(self.stderr(), "") self.assertEqual(self.stdout(), "runmod3: passed hello\n") def test_pkg1_main(self): run_python_module("pkg1", ["pkg1", "hello"]) + self.assertEqual(self.stderr(), "") self.assertEqual(self.stdout(), "pkg1.__main__: passed hello\n") def test_pkg1_sub_main(self): run_python_module("pkg1.sub", ["pkg1.sub", "hello"]) + self.assertEqual(self.stderr(), "") self.assertEqual(self.stdout(), "pkg1.sub.__main__: passed hello\n") def test_no_such_module(self): diff --git a/tests/test_farm.py b/tests/test_farm.py index 261ce4d0..b2ea3697 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -15,6 +15,10 @@ def test_farm(clean_only=False): yield (case,) +# "rU" was deprecated in 3.4 +READ_MODE = "rU" if sys.version_info < (3, 4) else "r" + + class FarmTestCase(object): """A test case from the farm tree. @@ -22,8 +26,8 @@ class FarmTestCase(object): copy("src", "out") run(''' - coverage -x white.py - coverage -a white.py + coverage run white.py + coverage annotate white.py ''', rundir="out") compare("out", "gold", "*,cover") clean("out") @@ -258,9 +262,9 @@ class FarmTestCase(object): # ourselves. text_diff = [] for f in diff_files: - with open(os.path.join(dir1, f), "rU") as fobj: + with open(os.path.join(dir1, f), READ_MODE) as fobj: left = fobj.read() - with open(os.path.join(dir2, f), "rU") as fobj: + with open(os.path.join(dir2, f), READ_MODE) as fobj: right = fobj.read() if scrubs: left = self._scrub(left, scrubs) @@ -280,7 +284,7 @@ class FarmTestCase(object): def _scrub(self, strdata, scrubs): """Scrub uninteresting data from the payload in `strdata`. - `scrubs is a list of (find, replace) pairs of regexes that are used on + `scrubs` is a list of (find, replace) pairs of regexes that are used on `strdata`. A string is returned. """ @@ -295,7 +299,8 @@ class FarmTestCase(object): missing in `filename`. """ - text = open(filename, "r").read() + with open(filename, "r") as fobj: + text = fobj.read() for s in strlist: assert s in text, "Missing content in %s: %r" % (filename, s) @@ -306,7 +311,8 @@ class FarmTestCase(object): `filename`. """ - text = open(filename, "r").read() + with open(filename, "r") as fobj: + text = fobj.read() for s in strlist: assert s not in text, "Forbidden content in %s: %r" % (filename, s) diff --git a/tests/test_files.py b/tests/test_files.py index f93feba7..070430ff 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -93,6 +93,12 @@ class MatcherTest(CoverageTest): for filepath, matches in matches_to_try: self.assertMatches(fnm, filepath, matches) + def test_fnmatch_matcher_overload(self): + fnm = FnmatchMatcher(["*x%03d*.txt" % i for i in range(500)]) + self.assertMatches(fnm, "x007foo.txt", True) + self.assertMatches(fnm, "x123foo.txt", True) + self.assertMatches(fnm, "x798bar.txt", False) + class PathAliasesTest(CoverageTest): """Tests for coverage/files.py:PathAliases""" @@ -131,11 +137,11 @@ class PathAliasesTest(CoverageTest): def test_cant_have_wildcard_at_end(self): aliases = PathAliases() msg = "Pattern must not end with wildcards." - with self.assertRaisesRegexp(CoverageException, msg): + with self.assertRaisesRegex(CoverageException, msg): aliases.add("/ned/home/*", "fooey") - with self.assertRaisesRegexp(CoverageException, msg): + with self.assertRaisesRegex(CoverageException, msg): aliases.add("/ned/home/*/", "fooey") - with self.assertRaisesRegexp(CoverageException, msg): + with self.assertRaisesRegex(CoverageException, msg): aliases.add("/ned/home/*/*/", "fooey") def test_no_accidental_munging(self): @@ -177,7 +183,7 @@ class RelativePathAliasesTest(CoverageTest): aliases.add(d, '/the/source') the_file = os.path.join(d, 'a.py') the_file = os.path.expanduser(the_file) - the_file = os.path.abspath(the_file) + the_file = os.path.abspath(os.path.realpath(the_file)) assert '~' not in the_file # to be sure the test is pure. self.assertEqual(aliases.map(the_file), '/the/source/a.py') diff --git a/tests/test_html.py b/tests/test_html.py index 41859382..8e43e7cf 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Tests that HTML generation is awesome.""" -import os.path, re +import os.path, re, sys import coverage import coverage.html from coverage.misc import CoverageException, NotPython, NoSource @@ -42,6 +42,13 @@ class HtmlTestHelpers(CoverageTest): os.remove("htmlcov/helper1.html") os.remove("htmlcov/helper2.html") + def get_html_report_content(self, module): + """Return the content of the HTML report for `module`.""" + filename = module.replace(".py", ".html").replace("/", "_") + filename = os.path.join("htmlcov", filename) + with open(filename) as f: + return f.read() + class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML delta speed-ups.""" @@ -208,7 +215,7 @@ class HtmlTitleTest(HtmlTestHelpers, CoverageTest): ) -class HtmlWithUnparsableFilesTest(CoverageTest): +class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): """Test the behavior when measuring unparsable files.""" def test_dotpy_not_python(self): @@ -217,7 +224,7 @@ class HtmlWithUnparsableFilesTest(CoverageTest): self.start_import_stop(cov, "innocuous") self.make_file("innocuous.py", "<h1>This isn't python!</h1>") msg = "Couldn't parse '.*innocuous.py' as Python source: .* at line 1" - with self.assertRaisesRegexp(NotPython, msg): + with self.assertRaisesRegex(NotPython, msg): cov.html_report() def test_dotpy_not_python_ignored(self): @@ -267,6 +274,31 @@ class HtmlWithUnparsableFilesTest(CoverageTest): cov.html_report() self.assert_exists("htmlcov/index.html") + def test_decode_error(self): + # imp.load_module won't load a file with an undecodable character + # in a comment, though Python will run them. So we'll change the + # file after running. + self.make_file("main.py", "import sub.not_ascii") + self.make_file("sub/__init__.py") + self.make_file("sub/not_ascii.py", """\ + a = 1 # Isn't this great?! + """) + cov = coverage.coverage() + self.start_import_stop(cov, "main") + + # Create the undecodable version of the file. + self.make_file("sub/not_ascii.py", """\ + a = 1 # Isn't this great?\xcb! + """) + cov.html_report() + + html_report = self.get_html_report_content("sub/not_ascii.py") + if sys.version_info < (3, 0): + expected = "# Isn't this great?�!" + else: + expected = "# Isn't this great?Ë!" + self.assertIn(expected, html_report) + class HtmlTest(CoverageTest): """Moar HTML tests.""" @@ -283,7 +315,7 @@ class HtmlTest(CoverageTest): missing_file = os.path.join(self.temp_dir, "sub", "another.py") missing_file = os.path.realpath(missing_file) msg = "(?i)No source for code: '%s'" % re.escape(missing_file) - with self.assertRaisesRegexp(NoSource, msg): + with self.assertRaisesRegex(NoSource, msg): cov.html_report() class HtmlStaticFileTest(CoverageTest): @@ -340,5 +372,5 @@ class HtmlStaticFileTest(CoverageTest): cov = coverage.coverage() self.start_import_stop(cov, "main") msg = "Couldn't find static file '.*'" - with self.assertRaisesRegexp(CoverageException, msg): + with self.assertRaisesRegex(CoverageException, msg): cov.html_report() diff --git a/tests/test_parser.py b/tests/test_parser.py index 5b90f342..a392ea03 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -13,7 +13,7 @@ class PythonParserTest(CoverageTest): def parse_source(self, text): """Parse `text` as source, and return the `PythonParser` used.""" text = textwrap.dedent(text) - parser = PythonParser(None, text=text, exclude="nocover") + parser = PythonParser(text=text, exclude="nocover") parser.parse_source() return parser @@ -98,7 +98,7 @@ class ParserFileTest(CoverageTest): def parse_file(self, filename): """Parse `text` as source, and return the `PythonParser` used.""" - parser = PythonParser(None, filename=filename, exclude="nocover") + parser = PythonParser(filename=filename, exclude="nocover") parser.parse_source() return parser diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index e15400b6..4755c167 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -97,6 +97,11 @@ if sys.version_info < (3, 0): source = "# This Python file uses this encoding: utf-8\n" self.assertEqual(source_encoding(source), 'utf-8') + def test_detect_source_encoding_not_in_comment(self): + # Should not detect anything here + source = 'def parse(src, encoding=None):\n pass' + self.assertEqual(source_encoding(source), 'ascii') + def test_detect_source_encoding_on_second_line(self): # A coding declaration should be found despite a first blank line. source = "\n# coding=cp850\n\n" diff --git a/tests/test_process.py b/tests/test_process.py index fa4759a8..d8314982 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -26,7 +26,7 @@ class ProcessTest(CoverageTest): """) self.assert_doesnt_exist(".coverage") - self.run_command("coverage -x mycode.py") + self.run_command("coverage run mycode.py") self.assert_exists(".coverage") def test_environment(self): @@ -39,7 +39,7 @@ class ProcessTest(CoverageTest): """) self.assert_doesnt_exist(".coverage") - out = self.run_command("coverage -x mycode.py") + out = self.run_command("coverage run mycode.py") self.assert_exists(".coverage") self.assertEqual(out, 'done\n') @@ -55,11 +55,11 @@ class ProcessTest(CoverageTest): print('done') """) - out = self.run_command("coverage -x -p b_or_c.py b") + out = self.run_command("coverage run -p b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") - out = self.run_command("coverage -x -p b_or_c.py c") + out = self.run_command("coverage run -p b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") @@ -67,7 +67,7 @@ class ProcessTest(CoverageTest): self.assertEqual(self.number_of_data_files(), 2) # Combine the parallel coverage data files into .coverage . - self.run_command("coverage -c") + self.run_command("coverage combine") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. @@ -91,23 +91,23 @@ class ProcessTest(CoverageTest): print('done') """) - out = self.run_command("coverage -x -p b_or_c.py b") + out = self.run_command("coverage run -p b_or_c.py b") self.assertEqual(out, 'done\n') self.assert_doesnt_exist(".coverage") self.assertEqual(self.number_of_data_files(), 1) # Combine the (one) parallel coverage data file into .coverage . - self.run_command("coverage -c") + self.run_command("coverage combine") self.assert_exists(".coverage") self.assertEqual(self.number_of_data_files(), 1) - out = self.run_command("coverage -x -p b_or_c.py c") + out = self.run_command("coverage run --append -p b_or_c.py c") self.assertEqual(out, 'done\n') self.assert_exists(".coverage") self.assertEqual(self.number_of_data_files(), 2) # Combine the parallel coverage data files into .coverage . - self.run_command("coverage -c") + self.run_command("coverage combine") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. @@ -229,7 +229,7 @@ class ProcessTest(CoverageTest): self.run_command("coverage run fleeting.py") os.remove("fleeting.py") out = self.run_command("coverage html -d htmlcov") - self.assertRegexpMatches(out, "No source for code: '.*fleeting.py'") + self.assertRegex(out, "No source for code: '.*fleeting.py'") self.assertNotIn("Traceback", out) # It happens that the code paths are different for *.py and other @@ -240,14 +240,14 @@ class ProcessTest(CoverageTest): self.run_command("coverage run fleeting") os.remove("fleeting") - status, out = self.run_command_status("coverage html -d htmlcov", 1) - self.assertRegexpMatches(out, "No source for code: '.*fleeting'") + status, out = self.run_command_status("coverage html -d htmlcov") + self.assertRegex(out, "No source for code: '.*fleeting'") 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") + status, out = self.run_command_status("coverage run xyzzy.py") + self.assertRegex(out, "No file to run: .*xyzzy.py") self.assertNotIn("raceback", out) self.assertNotIn("rror", out) self.assertEqual(status, 1) @@ -265,7 +265,7 @@ class ProcessTest(CoverageTest): # The important thing is for "coverage run" and "python" to report the # same traceback. - status, out = self.run_command_status("coverage run throw.py", 1) + status, out = self.run_command_status("coverage run throw.py") out2 = self.run_command("python throw.py") if '__pypy__' in sys.builtin_module_names: # Pypy has an extra frame in the traceback for some reason @@ -294,8 +294,8 @@ class ProcessTest(CoverageTest): # The important thing is for "coverage run" and "python" to have the # same output. No traceback. - status, out = self.run_command_status("coverage run exit.py", 17) - status2, out2 = self.run_command_status("python exit.py", 17) + status, out = self.run_command_status("coverage run exit.py") + status2, out2 = self.run_command_status("python exit.py") self.assertMultiLineEqual(out, out2) self.assertMultiLineEqual(out, "about to exit..\n") self.assertEqual(status, status2) @@ -310,8 +310,8 @@ class ProcessTest(CoverageTest): f1() """) - status, out = self.run_command_status("coverage run exit_none.py", 0) - status2, out2 = self.run_command_status("python exit_none.py", 0) + status, out = self.run_command_status("coverage run exit_none.py") + status2, out2 = self.run_command_status("python exit_none.py") self.assertMultiLineEqual(out, out2) self.assertMultiLineEqual(out, "about to exit quietly..\n") self.assertEqual(status, status2) @@ -378,7 +378,7 @@ class ProcessTest(CoverageTest): self.assertEqual(self.number_of_data_files(), 2) # Combine the parallel coverage data files into .coverage . - self.run_command("coverage -c") + self.run_command("coverage combine") self.assert_exists(".coverage") # After combining, there should be only the .coverage file. @@ -502,6 +502,18 @@ class ProcessTest(CoverageTest): # about 5. self.assertGreater(data.summary()['os.py'], 50) + def test_deprecation_warnings(self): + # Test that coverage doesn't trigger deprecation warnings. + # https://bitbucket.org/ned/coveragepy/issue/305/pendingdeprecationwarning-the-imp-module + self.make_file("allok.py", """\ + import warnings + warnings.simplefilter('default') + import coverage + print("No warnings!") + """) + out = self.run_command("python allok.py") + self.assertEqual(out, "No warnings!\n") + class AliasedCommandTest(CoverageTest): """Tests of the version-specific command aliases.""" @@ -556,32 +568,47 @@ class FailUnderTest(CoverageTest): def setUp(self): super(FailUnderTest, self).setUp() - self.make_file("fifty.py", """\ - # I have 50% coverage! + self.make_file("forty_two_plus.py", """\ + # I have 42.857% (3/7) coverage! a = 1 - if a > 2: - b = 3 - c = 4 + b = 2 + if a > 3: + b = 4 + c = 5 + d = 6 + e = 7 """) - st, _ = self.run_command_status("coverage run fifty.py", 0) + st, _ = self.run_command_status("coverage run forty_two_plus.py") + self.assertEqual(st, 0) + st, out = self.run_command_status("coverage report") self.assertEqual(st, 0) + self.assertEqual( + self.last_line_squeezed(out), + "forty_two_plus 7 4 43%" + ) def test_report(self): - st, _ = self.run_command_status("coverage report --fail-under=50", 0) + st, _ = self.run_command_status("coverage report --fail-under=42") + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage report --fail-under=43") self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage report --fail-under=51", 2) + st, _ = self.run_command_status("coverage report --fail-under=44") self.assertEqual(st, 2) def test_html_report(self): - st, _ = self.run_command_status("coverage html --fail-under=50", 0) + st, _ = self.run_command_status("coverage html --fail-under=42") self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage html --fail-under=51", 2) + st, _ = self.run_command_status("coverage html --fail-under=43") + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage html --fail-under=44") self.assertEqual(st, 2) def test_xml_report(self): - st, _ = self.run_command_status("coverage xml --fail-under=50", 0) + st, _ = self.run_command_status("coverage xml --fail-under=42") + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage xml --fail-under=43") self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage xml --fail-under=51", 2) + st, _ = self.run_command_status("coverage xml --fail-under=44") self.assertEqual(st, 2) diff --git a/tests/test_summary.py b/tests/test_summary.py index 29167bf8..7bd1c496 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -21,26 +21,10 @@ class SummaryTest(CoverageTest): # Parent class saves and restores sys.path, we can just modify it. sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules')) - def report_from_command(self, cmd): - """Return the report from the `cmd`, with some convenience added.""" - report = self.run_command(cmd).replace('\\', '/') - self.assertNotIn("error", report.lower()) - return report - - def line_count(self, report): - """How many lines are in `report`?""" - self.assertEqual(report.split('\n')[-1], "") - return len(report.split('\n')) - 1 - - def last_line_squeezed(self, report): - """Return the last line of `report` with the spaces squeezed down.""" - last_line = report.split('\n')[-2] - return re.sub(r"\s+", " ", last_line) - def test_report(self): - out = self.run_command("coverage -x mycode.py") + out = self.run_command("coverage run mycode.py") self.assertEqual(out, 'done\n') - report = self.report_from_command("coverage -r") + report = self.report_from_command("coverage report") # Name Stmts Miss Cover # --------------------------------------------------------------------- @@ -58,8 +42,24 @@ class SummaryTest(CoverageTest): def test_report_just_one(self): # Try reporting just one module - self.run_command("coverage -x mycode.py") - report = self.report_from_command("coverage -r mycode.py") + self.run_command("coverage run mycode.py") + report = self.report_from_command("coverage report mycode.py") + + # Name Stmts Miss Cover + # ---------------------------- + # mycode 4 0 100% + + self.assertEqual(self.line_count(report), 3) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/tests/modules/covmod1 ", report) + self.assertNotIn("/tests/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) + self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") + + def test_report_wildcard(self): + # Try reporting using wildcards to get the modules. + self.run_command("coverage run mycode.py") + report = self.report_from_command("coverage report my*.py") # Name Stmts Miss Cover # ---------------------------- @@ -75,8 +75,10 @@ class SummaryTest(CoverageTest): def test_report_omitting(self): # Try reporting while omitting some modules prefix = os.path.split(__file__)[0] - self.run_command("coverage -x mycode.py") - report = self.report_from_command("coverage -r -o '%s/*'" % prefix) + self.run_command("coverage run mycode.py") + report = self.report_from_command( + "coverage report --omit '%s/*'" % prefix + ) # Name Stmts Miss Cover # ---------------------------- @@ -126,13 +128,109 @@ class SummaryTest(CoverageTest): self.assertEqual(self.last_line_squeezed(report), "mybranch 5 0 2 1 86%") + def test_report_show_missing(self): + self.make_file("mymissing.py", """\ + def missing(x, y): + if x: + print("x") + return x + if y: + print("y") + try: + print("z") + 1/0 + print("Never!") + except ZeroDivisionError: + pass + return x + missing(0, 1) + """) + out = self.run_command("coverage run mymissing.py") + self.assertEqual(out, 'y\nz\n') + report = self.report_from_command("coverage report --show-missing") + + # Name Stmts Miss Cover Missing + # ----------------------------------------- + # mymissing 14 3 79% 3-4, 10 + + self.assertEqual(self.line_count(report), 3) + self.assertIn("mymissing ", report) + self.assertEqual(self.last_line_squeezed(report), + "mymissing 14 3 79% 3-4, 10") + + def test_report_show_missing_branches(self): + self.make_file("mybranch.py", """\ + def branch(x, y): + if x: + print("x") + if y: + print("y") + return x + branch(1, 1) + """) + out = self.run_command("coverage run --branch mybranch.py") + self.assertEqual(out, 'x\ny\n') + report = self.report_from_command("coverage report --show-missing") + + # Name Stmts Miss Branch BrMiss Cover Missing + # ------------------------------------------------------- + # mybranch 7 0 4 2 82% 2->4, 4->6 + + self.assertEqual(self.line_count(report), 3) + self.assertIn("mybranch ", report) + self.assertEqual(self.last_line_squeezed(report), + "mybranch 7 0 4 2 82% 2->4, 4->6") + + def test_report_show_missing_branches_and_lines(self): + self.make_file("main.py", """\ + import mybranch + """) + self.make_file("mybranch.py", """\ + def branch(x, y, z): + if x: + print("x") + if y: + print("y") + if z: + if x and y: + print("z") + return x + branch(1, 1, 0) + """) + out = self.run_command("coverage run --branch main.py") + self.assertEqual(out, 'x\ny\n') + report = self.report_from_command("coverage report --show-missing") + + # pylint: disable=C0301 + # Name Stmts Miss Branch BrMiss Cover Missing + # ------------------------------------------------------- + # main 1 0 0 0 100% + # mybranch 10 2 8 5 61% 7-8, 2->4, 4->6 + # ------------------------------------------------------- + # TOTAL 11 2 8 5 63% + + self.assertEqual(self.line_count(report), 6) + squeezed = self.squeezed_lines(report) + self.assertEqual( + squeezed[2], + "main 1 0 0 0 100%" + ) + self.assertEqual( + squeezed[3], + "mybranch 10 2 8 5 61% 7-8, 2->4, 4->6" + ) + self.assertEqual( + squeezed[5], + "TOTAL 11 2 8 5 63%" + ) + def test_dotpy_not_python(self): # We run a .py file, and when reporting, we can't parse it as Python. # We should get an error message in the report. self.run_command("coverage run mycode.py") self.make_file("mycode.py", "This isn't python at all!") - report = self.report_from_command("coverage -r mycode.py") + report = self.report_from_command("coverage report mycode.py") # pylint: disable=C0301 # Name Stmts Miss Cover @@ -155,7 +253,7 @@ class SummaryTest(CoverageTest): # but we've said to ignore errors, so there's no error reported. self.run_command("coverage run mycode.py") self.make_file("mycode.py", "This isn't python at all!") - report = self.report_from_command("coverage -r -i mycode.py") + report = self.report_from_command("coverage report -i mycode.py") # Name Stmts Miss Cover # ---------------------------- @@ -171,7 +269,7 @@ class SummaryTest(CoverageTest): self.run_command("coverage run mycode.html") # Before reporting, change it to be an HTML file. self.make_file("mycode.html", "<h1>This isn't python at all!</h1>") - report = self.report_from_command("coverage -r mycode.html") + report = self.report_from_command("coverage report mycode.html") # Name Stmts Miss Cover # ---------------------------- diff --git a/tests/test_templite.py b/tests/test_templite.py index 4b1f6e45..a4667a62 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -35,8 +35,12 @@ class TempliteTest(CoverageTest): self.assertEqual(actual, result) def assertSynErr(self, msg): + """Assert that a `TempliteSyntaxError` will happen. + + A context manager, and the message should be `msg`. + """ pat = "^" + re.escape(msg) + "$" - return self.assertRaisesRegexp(TempliteSyntaxError, pat) + return self.assertRaisesRegex(TempliteSyntaxError, pat) def test_passthrough(self): # Strings without variables are passed through unchanged. diff --git a/tests/test_testing.py b/tests/test_testing.py index a89a59a9..049a1982 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -12,13 +12,13 @@ class TestingTest(TestCase): run_in_temp_dir = False - def test_assert_same_elements(self): - self.assertSameElements(set(), set()) - self.assertSameElements(set([1,2,3]), set([3,1,2])) + def test_assert_count_equal(self): + self.assertCountEqual(set(), set()) + self.assertCountEqual(set([1,2,3]), set([3,1,2])) with self.assertRaises(AssertionError): - self.assertSameElements(set([1,2,3]), set()) + self.assertCountEqual(set([1,2,3]), set()) with self.assertRaises(AssertionError): - self.assertSameElements(set([1,2,3]), set([4,5,6])) + self.assertCountEqual(set([1,2,3]), set([4,5,6])) class CoverageTestTest(CoverageTest): |