diff options
45 files changed, 1564 insertions, 815 deletions
@@ -9,6 +9,8 @@ syntax: glob *.bak .coverage .coverage.* +.metacov +.metacov.* *.swp # Stuff generated by editors. diff --git a/AUTHORS.txt b/AUTHORS.txt index 3aa04adf..d374c2ac 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -32,6 +32,7 @@ Noel O'Boyle Detlev Offenbach JT Olds George Paci +Peter Portante Catherine Proulx Brandon Rhodes Adi Roiban diff --git a/CHANGES.txt b/CHANGES.txt index 539037a5..b034744f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,10 @@ 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. +- Gevent, eventlet, and greenlet are now supported, closing `issue 149`_. Huge + thanks to Peter Portante for initial implementation, and to Joe Jevnik for + the final insight that completed the work. + - 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`_. @@ -18,6 +22,9 @@ Change history for Coverage.py - The XML report now contains a <source> element, fixing `issue 94`_. Thanks Stan Hu. +- The class defined in the coverage module is now called ``Coverage`` instead + of ``coverage``, though the old name still works, for backward compatibility. + - The ``fail-under`` value is now rounded the same as reported results, preventing paradoxical results, fixing `issue 284`_. @@ -38,6 +45,7 @@ Change history for Coverage.py .. _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 149: https://bitbucket.org/ned/coveragepy/issue/149/coverage-gevent-looks-broken .. _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 @@ -3,9 +3,6 @@ default: @echo "* No default action *" -TEST_ZIP = tests/zipmods.zip -TEST_EGG = tests/eggsrc/dist/covtestegg1-0.0.0-py*.egg - clean: -rm -f *.pyd */*.pyd -rm -f *.so */*.so @@ -18,8 +15,8 @@ clean: -rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ -rm -f coverage/*,cover -rm -f MANIFEST - -rm -f .coverage .coverage.* coverage.xml - -rm -f $(TEST_ZIP) + -rm -f .coverage .coverage.* coverage.xml .metacov* + -rm -f tests/zipmods.zip -rm -rf tests/eggsrc/build tests/eggsrc/dist tests/eggsrc/*.egg-info -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz -rm -rf doc/_build diff --git a/coverage/__init__.py b/coverage/__init__.py index 193b7a10..5ae32aba 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -7,10 +7,14 @@ http://nedbatchelder.com/code/coverage from coverage.version import __version__, __url__ -from coverage.control import coverage, process_startup +from coverage.control import Coverage, process_startup from coverage.data import CoverageData from coverage.cmdline import main, CoverageScript from coverage.misc import CoverageException +from coverage.plugin import CoveragePlugin + +# Backward compatibility. +coverage = Coverage # Module-level functions. The original API to this module was based on # functions defined directly in the module, with a singleton of the coverage() diff --git a/tests/backunittest.py b/coverage/backunittest.py index 6498397f..b2b7ca2f 100644 --- a/tests/backunittest.py +++ b/coverage/backunittest.py @@ -23,14 +23,9 @@ class TestCase(unittest.TestCase): # 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)) + 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): diff --git a/coverage/backward.py b/coverage/backward.py index a7888a24..9597449c 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -50,22 +50,7 @@ else: if sys.version_info >= (3, 0): # Python 3.2 provides `tokenize.open`, the best way to open source files. import tokenize - try: - open_python_source = tokenize.open # pylint: disable=E1101 - except AttributeError: - from io import TextIOWrapper - detect_encoding = tokenize.detect_encoding # pylint: disable=E1101 - # Copied from the 3.2 stdlib: - def open_python_source(fname): - """Open a file in read only mode using the encoding detected by - detect_encoding(). - """ - buffer = open(fname, 'rb') - encoding, _ = detect_encoding(buffer.readline) - buffer.seek(0) - text = TextIOWrapper(buffer, encoding, line_buffering=True) - text.mode = 'r' - return text + open_python_source = tokenize.open # pylint: disable=E1101 else: def open_python_source(fname): """Open a source file the best way.""" @@ -118,14 +103,6 @@ else: for byte in bytes_value: yield ord(byte) -# Md5 is available in different places. -try: - import hashlib - md5 = hashlib.md5 -except ImportError: - import md5 - md5 = md5.new - try: # In Py 2.x, the builtins were in __builtin__ diff --git a/coverage/bytecode.py b/coverage/bytecode.py index 85360638..3f62dfaf 100644 --- a/coverage/bytecode.py +++ b/coverage/bytecode.py @@ -29,7 +29,6 @@ class ByteCodes(object): Returns `ByteCode` objects. """ - # pylint: disable=R0924 def __init__(self, code): self.code = code diff --git a/coverage/codeunit.py b/coverage/codeunit.py index 35167a72..c9ab2622 100644 --- a/coverage/codeunit.py +++ b/coverage/codeunit.py @@ -7,17 +7,17 @@ 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, get_ext=None): +def code_unit_factory(morfs, file_locator, get_plugin=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 + `get_plugin` is a function taking a filename, and returning a plugin + responsible for the file. It can also return None if there is no plugin + claiming the file. Returns a list of CodeUnit objects. @@ -26,15 +26,14 @@ def code_unit_factory(morfs, file_locator, get_ext=None): if not isinstance(morfs, (list, tuple)): morfs = [morfs] - django_tracer = DjangoTracer() - code_units = [] for morf in morfs: - ext = None - if isinstance(morf, string_class) and get_ext: - ext = get_ext(morf) - if ext: - klass = DjangoTracer # NOT REALLY! TODO + plugin = None + if isinstance(morf, string_class) and get_plugin: + plugin = get_plugin(morf) + if plugin: + klass = plugin.code_unit_class(morf) + #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! @@ -91,6 +90,8 @@ class CodeUnit(object): self.name = n self.modname = modname + self._source = None + def __repr__(self): return "<CodeUnit name=%r filename=%r>" % (self.name, self.filename) @@ -131,6 +132,11 @@ class CodeUnit(object): return root.replace('\\', '_').replace('/', '_').replace('.', '_') def source(self): + if self._source is None: + self._source = self.get_source() + return self._source + + def get_source(self): """Return the source code, as a string.""" if os.path.exists(self.filename): # A regular text file: open it. @@ -147,10 +153,9 @@ class CodeUnit(object): "No source for code '%s'." % self.filename ) - def source_token_lines(self, source): + def source_token_lines(self): """Return the 'tokenized' text for the code.""" - # TODO: Taking source here is wrong, change it? - for line in source.splitlines(): + for line in self.source().splitlines(): yield [('txt', line)] def should_be_python(self): @@ -162,6 +167,9 @@ class CodeUnit(object): """ return False + def get_parser(self, exclude=None): + raise NotImplementedError + class PythonCodeUnit(CodeUnit): """Represents a Python file.""" @@ -238,11 +246,11 @@ class PythonCodeUnit(CodeUnit): # Everything else is probably not Python. return False - def source_token_lines(self, source): - return source_token_lines(source) + def source_token_lines(self): + return source_token_lines(self.source()) - def source_encoding(self, source): - return source_encoding(source) + def source_encoding(self): + return source_encoding(self.source()) class MakoParser(CodeParser): @@ -271,26 +279,25 @@ class MakoCodeUnit(CodeUnit): py_source = open(self.filename).read() self.metadata = ModuleInfo.get_module_source_metadata(py_source, full_line_map=True) - def source(self): + def get_source(self): return open(self.metadata['filename']).read() def get_parser(self, exclude=None): return MakoParser(self.metadata) - def source_encoding(self, source): - # TODO: Taking source here is wrong, change it! + def source_encoding(self): return self.metadata['source_encoding'] class DjangoCodeUnit(CodeUnit): - def source(self): + def get_source(self): with open(self.filename) as f: return f.read() def get_parser(self, exclude=None): return DjangoParser(self.filename) - def source_encoding(self, source): + def source_encoding(self): return "utf8" diff --git a/coverage/collector.py b/coverage/collector.py index 546525d2..85c8dc90 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -1,6 +1,9 @@ """Raw data collector for Coverage.""" -import collections, os, sys, threading +import os, sys + +from coverage.misc import CoverageException +from coverage.pytracer import PyTracer try: # Use the C extension code when we can, for speed. @@ -21,188 +24,6 @@ except ImportError: CTracer = None -class PyTracer(object): - """Python implementation of the raw data tracer.""" - - # Because of poor implementations of trace-function-manipulating tools, - # the Python trace function must be kept very simple. In particular, there - # must be only one function ever set as the trace function, both through - # sys.settrace, and as the return value from the trace function. Put - # another way, the trace function must always return itself. It cannot - # swap in other functions, or return None to avoid tracing a particular - # frame. - # - # The trace manipulator that introduced this restriction is DecoratorTools, - # which sets a trace function, and then later restores the pre-existing one - # by calling sys.settrace with a function it found in the current frame. - # - # Systems that use DecoratorTools (or similar trace manipulations) must use - # PyTracer to get accurate results. The command-line --timid argument is - # 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.thread = None - self.stopped = False - self.coroutine_id_func = None - self.last_coroutine = None - - def _trace(self, frame, event, arg_unused): - """The trace function passed to sys.settrace.""" - - if self.stopped: - return - - if 0: - # 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: - # Someone forgot a return event. - if self.arcs and self.cur_file_data: - pair = (self.last_line, -self.last_exc_firstlineno) - self.cur_file_data[pair] = None - if self.coroutine_id_func: - self.data_stack = self.data_stacks[self.coroutine_id_func()] - self.handler, _, self.cur_file_data, self.last_line = self.data_stack.pop() - self.last_exc_back = None - - if event == 'call': - # Entering a new function context. Decide if we should trace - # in this file. - 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.extension, self.cur_tracename, self.cur_file_data, self.last_line)) - filename = frame.f_code.co_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 - # code block, indicated by (-1, n). - self.last_line = -1 - elif event == 'line': - # Record an executed line. - 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 - self.cur_file_data[(self.last_line, -first)] = None - # Leaving this function, pop the filename stack. - if self.coroutine_id_func: - self.data_stack = self.data_stacks[self.coroutine_id_func()] - self.last_coroutine = self.coroutine_id_func() - 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) - self.last_exc_back = frame.f_back - self.last_exc_firstlineno = frame.f_code.co_firstlineno - return self._trace - - def start(self): - """Start this Tracer. - - Return a Python function suitable for use with sys.settrace(). - - """ - self.thread = threading.currentThread() - sys.settrace(self._trace) - return self._trace - - def stop(self): - """Stop this Tracer.""" - self.stopped = True - if self.thread != threading.currentThread(): - # Called on a different thread than started us: we can't unhook - # ourseves, but we've set the flag that we should stop, so we won't - # do any more tracing. - return - - if hasattr(sys, "gettrace") and self.warn: - if sys.gettrace() != self._trace: - msg = "Trace function changed, measurement is likely wrong: %r" - self.warn(msg % (sys.gettrace(),)) - #print("Stopping tracer on %s" % threading.current_thread().ident) - sys.settrace(None) - - def get_stats(self): - """Return a dictionary of statistics, or None.""" - return None - - class Collector(object): """Collects trace data. @@ -224,13 +45,17 @@ class Collector(object): # the top, and resumed when they become the top again. _collectors = [] - def __init__(self, should_trace, timid, branch, warn, coroutine): + def __init__(self, + should_trace, check_include, timid, branch, warn, coroutine, + ): """Create a collector. `should_trace` is a function, taking a filename, and returning a canonicalized filename, or None depending on whether the file should be traced or not. + TODO: `check_include` + If `timid` is true, then a slower simpler trace function will be used. This is important for some environments where manipulation of tracing functions make the faster more sophisticated trace function not @@ -243,21 +68,44 @@ class Collector(object): `warn` is a warning function, taking a single string message argument, to be used if a warning needs to be issued. + TODO: `coroutine` + """ self.should_trace = should_trace + self.check_include = check_include self.warn = warn self.branch = branch - if coroutine == "greenlet": - import greenlet - self.coroutine_id_func = greenlet.getcurrent - elif coroutine == "eventlet": - import eventlet.greenthread - self.coroutine_id_func = eventlet.greenthread.getcurrent - elif coroutine == "gevent": - import gevent - self.coroutine_id_func = gevent.getcurrent - else: - self.coroutine_id_func = None + self.threading = None + self.coroutine = coroutine + + self.coroutine_id_func = None + + try: + if coroutine == "greenlet": + import greenlet + self.coroutine_id_func = greenlet.getcurrent + elif coroutine == "eventlet": + import eventlet.greenthread + self.coroutine_id_func = eventlet.greenthread.getcurrent + elif coroutine == "gevent": + import gevent + self.coroutine_id_func = gevent.getcurrent + elif coroutine == "thread" or not coroutine: + # It's important to import threading only if we need it. If + # it's imported early, and the program being measured uses + # gevent, then gevent's monkey-patching won't work properly. + import threading + self.threading = threading + else: + raise CoverageException( + "Don't understand coroutine=%s" % coroutine + ) + except ImportError: + raise CoverageException( + "Couldn't trace with coroutine=%s, " + "the module isn't installed." % coroutine + ) + self.reset() if timid: @@ -281,7 +129,7 @@ class Collector(object): # or mapping filenames to dicts with linenumber pairs as keys. self.data = {} - self.extensions = {} + self.plugin_data = {} # 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 @@ -299,12 +147,25 @@ class Collector(object): tracer.should_trace = self.should_trace tracer.should_trace_cache = self.should_trace_cache 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 + elif self.coroutine_id_func: + raise CoverageException( + "Can't support coroutine=%s with %s, " + "only threads are supported" % ( + self.coroutine, self.tracer_name(), + ) + ) + + if hasattr(tracer, 'plugin_data'): + tracer.plugin_data = self.plugin_data + if hasattr(tracer, 'threading'): + tracer.threading = self.threading + fn = tracer.start() self.tracers.append(tracer) + return fn # The trace function has to be set individually on each thread before @@ -331,16 +192,14 @@ class Collector(object): if self._collectors: self._collectors[-1].pause() self._collectors.append(self) - #print("Started: %r" % self._collectors, file=sys.stderr) # Check to see whether we had a fullcoverage tracer installed. traces0 = [] - if hasattr(sys, "gettrace"): - fn0 = sys.gettrace() - if fn0: - tracer0 = getattr(fn0, '__self__', None) - if tracer0: - traces0 = getattr(tracer0, 'traces', []) + fn0 = sys.gettrace() + if fn0: + tracer0 = getattr(fn0, '__self__', None) + if tracer0: + traces0 = getattr(tracer0, 'traces', []) # Install the tracer on this thread. fn = self._start_tracer() @@ -356,11 +215,11 @@ class Collector(object): # Install our installation tracer in threading, to jump start other # threads. - threading.settrace(self._installation_trace) + if self.threading: + self.threading.settrace(self._installation_trace) def stop(self): """Stop collecting trace information.""" - #print >>sys.stderr, "Stopping: %r" % self._collectors assert self._collectors assert self._collectors[-1] is self @@ -382,13 +241,17 @@ class Collector(object): print("\nCoverage.py tracer stats:") for k in sorted(stats.keys()): print("%16s: %s" % (k, stats[k])) - threading.settrace(None) + if self.threading: + self.threading.settrace(None) def resume(self): """Resume tracing after a `pause`.""" for tracer in self.tracers: tracer.start() - threading.settrace(self._installation_trace) + if self.threading: + self.threading.settrace(self._installation_trace) + else: + self._start_tracer() def get_line_data(self): """Return the line data collected. @@ -420,5 +283,5 @@ class Collector(object): else: return {} - def get_extension_data(self): - return self.extensions + def get_plugin_data(self): + return self.plugin_data diff --git a/coverage/config.py b/coverage/config.py index 064bc1ca..c671ef75 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -37,6 +37,13 @@ class HandyConfigParser(configparser.RawConfigParser): section = self.section_prefix + section return configparser.RawConfigParser.options(self, section) + def get_section(self, section): + """Get the contents of a section, as a dictionary.""" + d = {} + for opt in self.options(section): + d[opt] = self.get(section, opt) + return d + def get(self, section, *args, **kwargs): """Get a value, replacing environment variables also. @@ -140,7 +147,7 @@ class CoverageConfig(object): self.timid = False self.source = None self.debug = [] - self.extensions = [] + self.plugins = [] # Defaults for [report] self.exclude_list = DEFAULT_EXCLUDE[:] @@ -163,6 +170,9 @@ class CoverageConfig(object): # Defaults for [paths] self.paths = {} + # Options for plugins + self.plugin_options = {} + def from_environment(self, env_var): """Read configuration from the `env_var` environment variable.""" # Timidity: for nose users, read an environment variable. This is a @@ -172,7 +182,7 @@ class CoverageConfig(object): if env: self.timid = ('--timid' in env) - MUST_BE_LIST = ["omit", "include", "debug", "extensions"] + MUST_BE_LIST = ["omit", "include", "debug", "plugins"] def from_args(self, **kwargs): """Read config values from `kwargs`.""" @@ -200,17 +210,22 @@ class CoverageConfig(object): self.config_files.extend(files_read) for option_spec in self.CONFIG_FILE_OPTIONS: - self.set_attr_from_config_option(cp, *option_spec) + self._set_attr_from_config_option(cp, *option_spec) # [paths] is special if cp.has_section('paths'): for option in cp.options('paths'): self.paths[option] = cp.getlist('paths', option) + # plugins can have options + for plugin in self.plugins: + if cp.has_section(plugin): + self.plugin_options[plugin] = cp.get_section(plugin) + return True CONFIG_FILE_OPTIONS = [ - # These are *args for set_attr_from_config_option: + # These are *args for _set_attr_from_config_option: # (attr, where, type_="") # # attr is the attribute to set on the CoverageConfig object. @@ -224,7 +239,7 @@ class CoverageConfig(object): ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), - ('extensions', 'run:extensions', 'list'), + ('plugins', 'run:plugins', 'list'), ('include', 'run:include', 'list'), ('omit', 'run:omit', 'list'), ('parallel', 'run:parallel', 'boolean'), @@ -250,9 +265,13 @@ class CoverageConfig(object): ('xml_output', 'xml:output'), ] - def set_attr_from_config_option(self, cp, attr, where, type_=''): + def _set_attr_from_config_option(self, cp, attr, where, type_=''): """Set an attribute on self if it exists in the ConfigParser.""" section, option = where.split(":") if cp.has_option(section, option): method = getattr(cp, 'get'+type_) setattr(self, attr, method(section, option)) + + def get_plugin_options(self, plugin): + """Get a dictionary of options for the plugin named `plugin`.""" + return self.plugin_options.get(plugin, {}) diff --git a/coverage/control.py b/coverage/control.py index cb917e52..86a2ae23 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,6 +1,6 @@ """Core control stuff for Coverage.""" -import atexit, os, random, socket, sys +import atexit, os, platform, random, socket, sys from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems @@ -9,7 +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.plugin import Plugins, plugin_implements from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns from coverage.html import HtmlReporter @@ -28,14 +28,14 @@ except ImportError: _structseq = None -class coverage(object): +class Coverage(object): """Programmatic access to coverage.py. To use:: from coverage import coverage - cov = coverage() + cov = Coverage() cov.start() #.. call your code .. cov.stop() @@ -45,7 +45,7 @@ class coverage(object): def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, source=None, omit=None, include=None, debug=None, - debug_file=None, coroutine=None): + debug_file=None, coroutine=None, plugins=None): """ `data_file` is the base name of the data file to use, defaulting to ".coverage". `data_suffix` is appended (with a dot) to `data_file` to @@ -87,7 +87,9 @@ class coverage(object): `coroutine` is a string indicating the coroutining library being used in the measured code. Without this, coverage.py will get incorrect results. Valid strings are "greenlet", "eventlet", or "gevent", which - are all equivalent. + are all equivalent. TODO: really? + + `plugins` TODO. """ from coverage import __version__ @@ -126,15 +128,20 @@ class coverage(object): data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), source=source, omit=omit, include=include, debug=debug, - coroutine=coroutine, + coroutine=coroutine, plugins=plugins, ) # 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] + # Load plugins + self.plugins = Plugins.load_plugins(self.config.plugins, self.config) + + self.trace_judges = [] + for plugin in self.plugins: + if plugin_implements(plugin, "trace_judge"): + self.trace_judges.append(plugin) + self.trace_judges.append(None) # The Python case. self.auto_data = auto_data @@ -158,8 +165,11 @@ class coverage(object): self.include = prep_patterns(self.config.include) self.collector = Collector( - self._should_trace, timid=self.config.timid, - branch=self.config.branch, warn=self._warn, + should_trace=self._should_trace, + check_include=self._tracing_check_include_omit_etc, + timid=self.config.timid, + branch=self.config.branch, + warn=self._warn, coroutine=self.config.coroutine, ) @@ -186,18 +196,16 @@ class coverage(object): ) # The dirs for files considered "installed with the interpreter". - self.pylib_dirs = [] + self.pylib_dirs = set() if not self.config.cover_pylib: # Look at where some standard modules are located. That's the # indication for "installed with the interpreter". In some # environments (virtualenv, for example), these modules may be # spread across a few locations. Look at all the candidate modules # we've imported, and take all the different ones. - for m in (atexit, os, random, socket, _structseq): + for m in (atexit, os, platform, random, socket, _structseq): if m is not None and hasattr(m, "__file__"): - m_dir = self._canonical_dir(m) - if m_dir not in self.pylib_dirs: - self.pylib_dirs.append(m_dir) + self.pylib_dirs.add(self._canonical_dir(m)) # To avoid tracing the coverage code itself, we skip anything located # where we are. @@ -247,20 +255,10 @@ class coverage(object): """ disp = FileDisposition(filename) - - if not filename: - # Empty string is pretty useless - 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 disp.nope("not a real filename") + def nope(disp, reason): + disp.trace = False + disp.reason = reason + return disp self._check_for_packages() @@ -274,46 +272,80 @@ class coverage(object): if dunder_file: filename = self._source_for_file(dunder_file) + if not filename: + # Empty string is pretty useless + return nope(disp, "empty string isn't a filename") + + if filename.startswith('memory:'): + return nope(disp, "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 nope(disp, "not a real filename") + # Jython reports the .class file to the tracer, use the source file. if filename.endswith("$py.class"): filename = filename[:-9] + ".py" canonical = self.file_locator.canonical_filename(filename) + disp.canonical_filename = canonical + + # Try the plugins, see if they have an opinion about the file. + for plugin in self.trace_judges: + if plugin: + plugin.trace_judge(disp) + else: + disp.trace = True + disp.source_filename = canonical + if disp.trace: + disp.plugin = plugin + + if disp.check_filters: + reason = self._check_include_omit_etc(disp.source_filename) + if reason: + nope(disp, reason) + + return disp + + return nope(disp, "no plugin found") # TODO: a test that causes this. - # 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 + def _check_include_omit_etc(self, filename): + """Check a filename against the include, omit, etc, rules. + Returns a string or None. String means, don't trace, and is the reason + why. None means no reason found to not trace. + + """ # 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 disp.nope("falls outside the --source trees") + if not self.source_match.match(filename): + return "falls outside the --source trees" elif self.include_match: - if not self.include_match.match(canonical): - return disp.nope("falls outside the --include trees") + if not self.include_match.match(filename): + return "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 disp.nope("is in the stdlib") + if self.pylib_match and self.pylib_match.match(filename): + return "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 disp.nope("is part of coverage.py") + if self.cover_match and self.cover_match.match(filename): + return "is part of coverage.py" # Check the file against the omit pattern. - if self.omit_match and self.omit_match.match(canonical): - return disp.nope("is inside an --omit pattern") + if self.omit_match and self.omit_match.match(filename): + return "is inside an --omit pattern" - disp.filename = canonical - return disp + # No reason found to skip this file. + return None def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -326,6 +358,22 @@ class coverage(object): self.debug.write(disp.debug_message()) return disp + def _tracing_check_include_omit_etc(self, filename): + """Check a filename against the include, omit, etc, rules, and say so. + + Returns a boolean: True if the file should be traced, False if not. + + """ + reason = self._check_include_omit_etc(filename) + if self.debug.should('trace'): + if not reason: + msg = "Tracing %r" % (filename,) + else: + msg = "Not tracing %r: %s" % (filename, reason) + self.debug.write(msg) + + return not reason + def _warn(self, msg): """Use `msg` as a warning.""" self._warnings.append(msg) @@ -545,7 +593,7 @@ class coverage(object): # 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.data.add_plugin_data(self.collector.get_plugin_data()) self.collector.reset() # If there are still entries in the source_pkgs list, then we never @@ -611,10 +659,17 @@ class coverage(object): Returns an `Analysis` object. """ + def get_plugin(filename): + """For code_unit_factory to use to find the plugin for a file.""" + plugin = None + plugin_name = self.data.plugin_data().get(filename) + if plugin_name: + plugin = self.plugins.get(plugin_name) + return plugin + self._harvest_data() if not isinstance(it, CodeUnit): - get_ext = self.data.extension_data().get - it = code_unit_factory(it, self.file_locator, get_ext)[0] + it = code_unit_factory(it, self.file_locator, get_plugin)[0] return Analysis(self, it) @@ -738,7 +793,6 @@ class coverage(object): """Return a list of (key, value) pairs showing internal information.""" import coverage as covmod - import platform, re try: implementation = platform.python_implementation() @@ -760,10 +814,10 @@ class coverage(object): ('executable', sys.executable), ('cwd', os.getcwd()), ('path', sys.path), - ('environment', sorted([ + ('environment', sorted( ("%s = %s" % (k, v)) for k, v in iitems(os.environ) - if re.search(r"^COV|^PY", k) - ])), + if k.startswith(("COV", "PY")) + )), ('command_line', " ".join(getattr(sys, 'argv', ['???']))), ] if self.source_match: @@ -784,21 +838,19 @@ 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.canonical_filename = original_filename + self.source_filename = None + self.check_filters = True + self.trace = False self.reason = "" - self.extension = None - - def nope(self, reason): - """A helper for returning a NO answer from should_trace.""" - self.reason = reason - return self + self.plugin = None 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: + if self.trace: msg = "Tracing %r" % (self.original_filename,) + else: + msg = "Not tracing %r: %s" % (self.original_filename, self.reason) return msg @@ -824,7 +876,7 @@ def process_startup(): """ cps = os.environ.get("COVERAGE_PROCESS_START") if cps: - cov = coverage(config_file=cps, auto_data=True) + cov = Coverage(config_file=cps, auto_data=True) cov.start() cov._warn_no_data = False cov._warn_unimported_source = False diff --git a/coverage/data.py b/coverage/data.py index b78c931d..e220a364 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -21,9 +21,9 @@ 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: + * plugins: a dict mapping filenames to plugin names: { 'file1': "django.coverage", ... } - # TODO: how to handle the difference between a extension module + # TODO: how to handle the difference between a plugin module # name, and the class in the module? """ @@ -69,13 +69,13 @@ class CoverageData(object): # self.arcs = {} - # A map from canonical source file name to an extension module name: + # A map from canonical source file name to an plugin module name: # # { # 'filename1.py': 'django.coverage', # ... # } - self.extensions = {} + self.plugins = {} def usefile(self, use_file=True): """Set whether or not to use a disk file for data.""" @@ -123,8 +123,8 @@ class CoverageData(object): (f, sorted(amap.keys())) for f, amap in iitems(self.arcs) ) - def extension_data(self): - return self.extensions + def plugin_data(self): + return self.plugins def write_file(self, filename): """Write the coverage data to `filename`.""" @@ -229,8 +229,8 @@ 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 add_plugin_data(self, plugin_data): + self.plugins.update(plugin_data) def touch_file(self, filename): """Ensure that `filename` appears in the data, empty if needed.""" diff --git a/coverage/debug.py b/coverage/debug.py index 6908383d..6e7af242 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -45,7 +45,7 @@ def info_formatter(info): for label, data in info: if data == []: data = "-none-" - if isinstance(data, (list, tuple)): + if isinstance(data, (list, set, tuple)): prefix = "%*s:" % (label_len, label) for e in data: yield "%*s %s" % (label_len+1, prefix, e) diff --git a/coverage/extension.py b/coverage/extension.py deleted file mode 100644 index 8c89b88e..00000000 --- a/coverage/extension.py +++ /dev/null @@ -1,20 +0,0 @@ -"""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 08ce1e84..1ed7276e 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -147,7 +147,7 @@ def prep_patterns(patterns): class TreeMatcher(object): """A matcher for files in a tree.""" def __init__(self, directories): - self.dirs = directories[:] + self.dirs = list(directories) def __repr__(self): return "<TreeMatcher %r>" % self.dirs @@ -177,7 +177,17 @@ 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])) + # fnmatch is platform-specific. On Windows, it does the Windows thing + # of treating / and \ as equivalent. But on other platforms, we need to + # take care of that ourselves. + fnpats = (fnmatch.translate(p) for p in pats) + fnpats = (p.replace(r"\/", r"[\\/]") for p in fnpats) + if sys.platform == 'win32': + # Windows is also case-insensitive. BTW: the regex docs say that + # flags like (?i) have to be at the beginning, but fnmatch puts + # them at the end, and have two there seems to work fine. + fnpats = (p + "(?i)" for p in fnpats) + self.re = re.compile(join_regex(fnpats)) def __repr__(self): return "<FnmatchMatcher %r>" % self.pats diff --git a/coverage/html.py b/coverage/html.py index 6e21efaa..91ae2c27 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -165,7 +165,7 @@ class HtmlReporter(Reporter): # If need be, determine the encoding of the source file. We use it # later to properly write the HTML. if sys.version_info < (3, 0): - encoding = cu.source_encoding(source) + encoding = cu.source_encoding() # Some UTF8 files have the dreaded UTF8 BOM. If so, junk it. if encoding.startswith("utf-8") and source[:3] == "\xef\xbb\xbf": source = source[3:] @@ -185,7 +185,7 @@ class HtmlReporter(Reporter): lines = [] - for lineno, line in enumerate(cu.source_token_lines(source), start=1): + for lineno, line in enumerate(cu.source_token_lines(), start=1): # Figure out how to mark this line. line_class = [] annotate_html = "" diff --git a/coverage/misc.py b/coverage/misc.py index 4b1dccb2..6962ae32 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -1,10 +1,10 @@ """Miscellaneous stuff for Coverage.""" import errno +import hashlib import inspect import os -from coverage.backward import md5 from coverage.backward import string_class, to_bytes @@ -86,12 +86,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) - elif regexes: - return regexes[0] - else: - return "" + return "|".join("(?:%s)" % r for r in regexes) def file_be_gone(path): @@ -106,7 +101,7 @@ def file_be_gone(path): class Hasher(object): """Hashes Python data into md5.""" def __init__(self): - self.md5 = md5() + self.md5 = hashlib.md5() def update(self, v): """Add `v` to the hash, recursively if needed.""" diff --git a/coverage/plugin.py b/coverage/plugin.py new file mode 100644 index 00000000..35be41a9 --- /dev/null +++ b/coverage/plugin.py @@ -0,0 +1,108 @@ +"""Plugin management for coverage.py""" + +import sys + + +class CoveragePlugin(object): + """Base class for coverage.py plugins.""" + def __init__(self, options): + self.options = options + + def trace_judge(self, disposition): + """Decide whether to trace this file with this plugin. + + Set disposition.trace to True if this plugin should trace this file. + May also set other attributes in `disposition`. + + """ + return None + + def source_file_name(self, filename): + """Return the source name for a given Python filename. + + Can return None if tracing shouldn't continue. + + """ + return filename + + def dynamic_source_file_name(self): + """Returns a callable that can return a source name for a frame. + + The callable should take a filename and a frame, and return either a + filename or None: + + def dynamic_source_filename_func(filename, frame) + + Can return None if dynamic filenames aren't needed. + + """ + return None + + def code_unit_class(self, morf): + """Return the CodeUnit class to use for a module or filename.""" + return None + + +class Plugins(object): + """The currently loaded collection of coverage.py plugins.""" + + def __init__(self): + self.order = [] + self.names = {} + + @classmethod + def load_plugins(cls, modules, config): + """Load plugins from `modules`. + + Returns a list of loaded and configured plugins. + + """ + plugins = cls() + + for module in modules: + __import__(module) + mod = sys.modules[module] + + plugin_class = getattr(mod, "Plugin", None) + if plugin_class: + options = config.get_plugin_options(module) + plugin = plugin_class(options) + plugin.__name__ = module + plugins.order.append(plugin) + plugins.names[module] = plugin + + return plugins + + def __iter__(self): + return iter(self.order) + + def get(self, module): + return self.names[module] + + +def overrides(obj, method_name, base_class): + """Does `obj` override the `method_name` it got from `base_class`? + + Determine if `obj` implements the method called `method_name`, which it + inherited from `base_class`. + + Returns a boolean. + + """ + klass = obj.__class__ + klass_func = getattr(klass, method_name) + base_func = getattr(base_class, method_name) + + # Python 2/3 compatibility: Python 2 returns an instancemethod object, the + # function is the .im_func attribute. Python 3 returns a plain function + # object already. + if sys.version_info < (3, 0): + klass_func = klass_func.im_func + base_func = base_func.im_func + + return klass_func is not base_func + + +def plugin_implements(obj, method_name): + """Does the plugin `obj` implement `method_name`?""" + return overrides(obj, method_name, CoveragePlugin) diff --git a/coverage/pytracer.py b/coverage/pytracer.py new file mode 100644 index 00000000..7563ae11 --- /dev/null +++ b/coverage/pytracer.py @@ -0,0 +1,163 @@ +"""Raw data collector for Coverage.""" + +import sys + + +class PyTracer(object): + """Python implementation of the raw data tracer.""" + + # Because of poor implementations of trace-function-manipulating tools, + # the Python trace function must be kept very simple. In particular, there + # must be only one function ever set as the trace function, both through + # sys.settrace, and as the return value from the trace function. Put + # another way, the trace function must always return itself. It cannot + # swap in other functions, or return None to avoid tracing a particular + # frame. + # + # The trace manipulator that introduced this restriction is DecoratorTools, + # which sets a trace function, and then later restores the pre-existing one + # by calling sys.settrace with a function it found in the current frame. + # + # Systems that use DecoratorTools (or similar trace manipulations) must use + # PyTracer to get accurate results. The command-line --timid argument is + # 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.plugin_data = None + # The threading module to use, if any. + self.threading = None + + self.plugin = [] + self.cur_file_dict = [] + self.last_line = [0] + + self.data_stack = [] + self.last_exc_back = None + self.last_exc_firstlineno = 0 + self.thread = None + self.stopped = False + + def __repr__(self): + return "<PyTracer at 0x{0:0x}: {1} lines in {2} files>".format( + id(self), + sum(len(v) for v in self.data.values()), + len(self.data), + ) + + def _trace(self, frame, event, arg_unused): + """The trace function passed to sys.settrace.""" + + if self.stopped: + return + + if self.last_exc_back: # TODO: bring this up to speed + if frame == self.last_exc_back: + # Someone forgot a return event. + if self.arcs and self.cur_file_dict: + pair = (self.last_line, -self.last_exc_firstlineno) + self.cur_file_dict[pair] = None + self.plugin, self.cur_file_dict, self.last_line = ( + self.data_stack.pop() + ) + self.last_exc_back = None + + if event == 'call': + # Entering a new function context. Decide if we should trace + # in this file. + self.data_stack.append( + (self.plugin, self.cur_file_dict, self.last_line) + ) + filename = frame.f_code.co_filename + disp = self.should_trace_cache.get(filename) + if disp is None: + disp = self.should_trace(filename, frame) + self.should_trace_cache[filename] = disp + + self.plugin = None + self.cur_file_dict = None + if disp.trace: + tracename = disp.source_filename + if disp.plugin: + dyn_func = disp.plugin.dynamic_source_file_name() + if dyn_func: + tracename = dyn_func(tracename, frame) + if tracename: + if not self.check_include(tracename): + tracename = None + else: + tracename = None + if tracename: + if tracename not in self.data: + self.data[tracename] = {} + if disp.plugin: + self.plugin_data[tracename] = disp.plugin.__name__ + self.cur_file_dict = self.data[tracename] + self.plugin = disp.plugin + # Set the last_line to -1 because the next arc will be entering a + # code block, indicated by (-1, n). + self.last_line = -1 + elif event == 'line': + # Record an executed line. + if self.plugin: + lineno_from, lineno_to = self.plugin.line_number_range(frame) + else: + lineno_from, lineno_to = frame.f_lineno, frame.f_lineno + if lineno_from != -1: + if self.cur_file_dict is not None: + if self.arcs: + self.cur_file_dict[ + (self.last_line, lineno_from) + ] = None + else: + for lineno in range(lineno_from, lineno_to+1): + self.cur_file_dict[lineno] = None + self.last_line = lineno_to + elif event == 'return': + if self.arcs and self.cur_file_dict: + first = frame.f_code.co_firstlineno + self.cur_file_dict[(self.last_line, -first)] = None + # Leaving this function, pop the filename stack. + self.plugin, self.cur_file_dict, self.last_line = ( + self.data_stack.pop() + ) + elif event == 'exception': + self.last_exc_back = frame.f_back + self.last_exc_firstlineno = frame.f_code.co_firstlineno + return self._trace + + def start(self): + """Start this Tracer. + + Return a Python function suitable for use with sys.settrace(). + + """ + if self.threading: + self.thread = self.threading.currentThread() + sys.settrace(self._trace) + return self._trace + + def stop(self): + """Stop this Tracer.""" + self.stopped = True + if self.threading and self.thread != self.threading.currentThread(): + # Called on a different thread than started us: we can't unhook + # ourseves, but we've set the flag that we should stop, so we won't + # do any more tracing. + return + + if self.warn: + if sys.gettrace() != self._trace: + msg = "Trace function changed, measurement is likely wrong: %r" + self.warn(msg % (sys.gettrace(),)) + + sys.settrace(None) + + def get_stats(self): + """Return a dictionary of statistics, or None.""" + return None diff --git a/coverage/report.py b/coverage/report.py index 7627d1aa..b93749c8 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -33,8 +33,8 @@ class Reporter(object): """ morfs = morfs or self.coverage.data.measured_files() file_locator = self.coverage.file_locator - get_ext = self.coverage.data.extension_data().get - self.code_units = code_unit_factory(morfs, file_locator, get_ext) + get_plugin = self.coverage.data.plugin_data().get + self.code_units = code_unit_factory(morfs, file_locator, get_plugin) if self.config.include: patterns = prep_patterns(self.config.include) diff --git a/coverage/summary.py b/coverage/summary.py index a6768cf9..9d31c226 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -71,7 +71,7 @@ class SummaryReporter(Reporter): total += nums except KeyboardInterrupt: # pragma: not covered raise - except: + except Exception: report_it = not self.config.ignore_errors if report_it: typ, msg = sys.exc_info()[:2] diff --git a/coverage/test_helpers.py b/coverage/test_helpers.py new file mode 100644 index 00000000..efe68dcd --- /dev/null +++ b/coverage/test_helpers.py @@ -0,0 +1,258 @@ +"""Mixin classes to help make good tests.""" + +import atexit +import collections +import os +import random +import shutil +import sys +import tempfile +import textwrap + +from coverage.backunittest import TestCase +from coverage.backward import StringIO, to_bytes + + +class Tee(object): + """A file-like that writes to all the file-likes it has.""" + + def __init__(self, *files): + """Make a Tee that writes to all the files in `files.`""" + self._files = files + if hasattr(files[0], "encoding"): + self.encoding = files[0].encoding + + def write(self, data): + """Write `data` to all the files.""" + for f in self._files: + f.write(data) + + if 0: + # Use this if you need to use a debugger, though it makes some tests + # fail, I'm not sure why... + def __getattr__(self, name): + return getattr(self._files[0], name) + + +class ModuleAwareMixin(TestCase): + """A test case mixin that isolates changes to sys.modules.""" + + def setUp(self): + super(ModuleAwareMixin, self).setUp() + + # Record sys.modules here so we can restore it in tearDown. + self.old_modules = dict(sys.modules) + self.addCleanup(self.cleanup_modules) + + def cleanup_modules(self): + """Remove any new modules imported during the test run. + + This lets us import the same source files for more than one test. + + """ + for m in [m for m in sys.modules if m not in self.old_modules]: + del sys.modules[m] + + +class SysPathAwareMixin(TestCase): + """A test case mixin that isolates changes to sys.path.""" + + def setUp(self): + super(SysPathAwareMixin, self).setUp() + + self.old_syspath = sys.path[:] + self.addCleanup(self.cleanup_syspath) + + def cleanup_syspath(self): + """Restore the original sys.path.""" + sys.path = self.old_syspath + + +class EnvironmentAwareMixin(TestCase): + """A test case mixin that isolates changes to the environment.""" + + def setUp(self): + super(EnvironmentAwareMixin, self).setUp() + + # Record environment variables that we changed with set_environ. + self.environ_undos = {} + + self.addCleanup(self.cleanup_environ) + + def set_environ(self, name, value): + """Set an environment variable `name` to be `value`. + + The environment variable is set, and record is kept that it was set, + so that `tearDown` can restore its original value. + + """ + if name not in self.environ_undos: + self.environ_undos[name] = os.environ.get(name) + os.environ[name] = value + + def cleanup_environ(self): + """Undo all the changes made by `set_environ`.""" + for name, value in self.environ_undos.items(): + if value is None: + del os.environ[name] + else: + os.environ[name] = value + + +class StdStreamCapturingMixin(TestCase): + """A test case mixin that captures stdout and stderr.""" + + def setUp(self): + super(StdStreamCapturingMixin, self).setUp() + + # Capture stdout and stderr so we can examine them in tests. + # nose keeps stdout from littering the screen, so we can safely Tee it, + # but it doesn't capture stderr, so we don't want to Tee stderr to the + # real stderr, since it will interfere with our nice field of dots. + self.old_stdout = sys.stdout + self.captured_stdout = StringIO() + sys.stdout = Tee(sys.stdout, self.captured_stdout) + self.old_stderr = sys.stderr + self.captured_stderr = StringIO() + sys.stderr = self.captured_stderr + + self.addCleanup(self.cleanup_std_streams) + + def cleanup_std_streams(self): + """Restore stdout and stderr.""" + sys.stdout = self.old_stdout + sys.stderr = self.old_stderr + + def stdout(self): + """Return the data written to stdout during the test.""" + return self.captured_stdout.getvalue() + + def stderr(self): + """Return the data written to stderr during the test.""" + return self.captured_stderr.getvalue() + + +class TempDirMixin(TestCase): + """A test case mixin that creates a temp directory and files in it.""" + + # Our own setting: most of these tests run in their own temp directory. + run_in_temp_dir = True + + def setUp(self): + super(TempDirMixin, self).setUp() + + if self.run_in_temp_dir: + # Create a temporary directory. + noise = str(random.random())[2:] + self.temp_root = os.path.join(tempfile.gettempdir(), 'test_cover') + self.temp_dir = os.path.join(self.temp_root, noise) + os.makedirs(self.temp_dir) + self.old_dir = os.getcwd() + os.chdir(self.temp_dir) + + # Modules should be importable from this temp directory. We don't + # use '' because we make lots of different temp directories and + # nose's caching importer can get confused. The full path prevents + # problems. + sys.path.insert(0, os.getcwd()) + + class_behavior = self.class_behavior() + class_behavior.tests += 1 + class_behavior.test_method_made_any_files = False + class_behavior.temp_dir = self.run_in_temp_dir + + self.addCleanup(self.cleanup_temp_dir) + + def cleanup_temp_dir(self): + """Clean up the temp directories we made.""" + + if self.run_in_temp_dir: + # Get rid of the temporary directory. + os.chdir(self.old_dir) + shutil.rmtree(self.temp_root) + + class_behavior = self.class_behavior() + if class_behavior.test_method_made_any_files: + class_behavior.tests_making_files += 1 + + def make_file(self, filename, text="", newline=None): + """Create a file for testing. + + `filename` is the relative path to the file, including directories if + desired, which will be created if need be. `text` is the content to + create in the file. If `newline` is provided, it is a string that will + be used as the line endings in the created file, otherwise the line + endings are as provided in `text`. + + Returns `filename`. + + """ + # Tests that call `make_file` should be run in a temp environment. + assert self.run_in_temp_dir + self.class_behavior().test_method_made_any_files = True + + text = textwrap.dedent(text) + if newline: + text = text.replace("\n", newline) + + # Make sure the directories are available. + dirs, _ = os.path.split(filename) + if dirs and not os.path.exists(dirs): + os.makedirs(dirs) + + # Create the file. + with open(filename, 'wb') as f: + f.write(to_bytes(text)) + + return filename + + # 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 + # option properly, so we track information about what the tests did, and + # then report at the end of the process on test classes that were set + # wrong. + + class ClassBehavior(object): + """A value object to store per-class.""" + def __init__(self): + self.tests = 0 + self.temp_dir = True + self.tests_making_files = 0 + self.test_method_made_any_files = False + + # Map from class to info about how it ran. + class_behaviors = collections.defaultdict(ClassBehavior) + + @classmethod + def report_on_class_behavior(cls): + """Called at process exit to report on class behavior.""" + for test_class, behavior in cls.class_behaviors.items(): + if behavior.temp_dir and behavior.tests_making_files == 0: + bad = "Inefficient" + elif not behavior.temp_dir and behavior.tests_making_files > 0: + bad = "Unsafe" + else: + bad = "" + + if bad: + if behavior.temp_dir: + where = "in a temp directory" + else: + where = "without a temp directory" + print( + "%s: %s ran %d tests, %d made files %s" % ( + bad, + test_class.__name__, + behavior.tests, + behavior.tests_making_files, + where, + ) + ) + + def class_behavior(self): + """Get the ClassBehavior instance for this test.""" + return self.class_behaviors[self.__class__] + +# When the process ends, find out about bad classes. +atexit.register(TempDirMixin.report_on_class_behavior) diff --git a/coverage/tracer.c b/coverage/tracer.c index ca8d61c1..5bf5c462 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -30,6 +30,7 @@ #define MyText_AS_BYTES(o) PyUnicode_AsASCIIString(o) #define MyText_AS_STRING(o) PyBytes_AS_STRING(o) #define MyInt_FromLong(l) PyLong_FromLong(l) +#define MyInt_AsLong(o) PyLong_AsLong(o) #define MyType_HEAD_INIT PyVarObject_HEAD_INIT(NULL, 0) @@ -40,6 +41,7 @@ #define MyText_AS_BYTES(o) (Py_INCREF(o), o) #define MyText_AS_STRING(o) PyString_AS_STRING(o) #define MyInt_FromLong(l) PyInt_FromLong(l) +#define MyInt_AsLong(o) PyInt_AsLong(o) #define MyType_HEAD_INIT PyObject_HEAD_INIT(NULL) 0, @@ -54,10 +56,23 @@ frame. */ typedef struct { - PyObject * file_data; /* PyMem_Malloc'ed, a borrowed ref. */ + /* The current file_data dictionary. Borrowed. */ + PyObject * file_data; + + /* The line number of the last line recorded, for tracing arcs. + -1 means there was no previous line, as when entering a code object. + */ int last_line; } DataStackEntry; +/* A data stack is a dynamically allocated vector of DataStackEntry's. */ +typedef struct { + int depth; /* The index of the last-used entry in stack. */ + int alloc; /* number of entries allocated at stack. */ + /* The file data at each level, or NULL if not recording. */ + DataStackEntry * stack; +} DataStack; + /* The CTracer type. */ typedef struct { @@ -66,7 +81,9 @@ typedef struct { /* Python objects manipulated directly by the Collector class. */ PyObject * should_trace; PyObject * warn; + PyObject * coroutine_id_func; PyObject * data; + PyObject * plugin_data; PyObject * should_trace_cache; PyObject * arcs; @@ -86,19 +103,17 @@ typedef struct { the keys are line numbers. In both cases, the value is irrelevant (None). */ - /* The index of the last-used entry in data_stack. */ - int depth; - /* The file data at each level, or NULL if not recording. */ - DataStackEntry * data_stack; - int data_stack_alloc; /* number of entries allocated at data_stack. */ - /* The current file_data dictionary. Borrowed. */ - PyObject * cur_file_data; + DataStack data_stack; /* Used if we aren't doing coroutines. */ + PyObject * data_stack_index; /* Used if we are doing coroutines. */ + DataStack * data_stacks; + int data_stacks_alloc; + int data_stacks_used; - /* The line number of the last line recorded, for tracing arcs. - -1 means there was no previous line, as when entering a code object. - */ - int last_line; + DataStack * pdata_stack; + + /* The current file's data stack entry, copied from the stack. */ + DataStackEntry cur_entry; /* The parent frame for the last exception event, to fix missing returns. */ PyFrameObject * last_exc_back; @@ -119,9 +134,47 @@ typedef struct { #endif /* COLLECT_STATS */ } CTracer; + #define STACK_DELTA 100 static int +DataStack_init(CTracer *self, DataStack *pdata_stack) +{ + pdata_stack->depth = -1; + pdata_stack->stack = NULL; + pdata_stack->alloc = 0; + return RET_OK; +} + +static void +DataStack_dealloc(CTracer *self, DataStack *pdata_stack) +{ + PyMem_Free(pdata_stack->stack); +} + +static int +DataStack_grow(CTracer *self, DataStack *pdata_stack) +{ + pdata_stack->depth++; + if (pdata_stack->depth >= pdata_stack->alloc) { + STATS( self->stats.stack_reallocs++; ) + /* We've outgrown our data_stack array: make it bigger. */ + int bigger = pdata_stack->alloc + STACK_DELTA; + DataStackEntry * bigger_data_stack = PyMem_Realloc(pdata_stack->stack, bigger * sizeof(DataStackEntry)); + if (bigger_data_stack == NULL) { + STATS( self->stats.errors++; ) + PyErr_NoMemory(); + pdata_stack->depth--; + return RET_ERROR; + } + pdata_stack->stack = bigger_data_stack; + pdata_stack->alloc = bigger; + } + return RET_OK; +} + + +static int CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused) { #if COLLECT_STATS @@ -138,24 +191,32 @@ CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused) self->should_trace = NULL; self->warn = NULL; + self->coroutine_id_func = NULL; self->data = NULL; + self->plugin_data = NULL; self->should_trace_cache = NULL; self->arcs = NULL; self->started = 0; self->tracing_arcs = 0; - self->depth = -1; - self->data_stack = PyMem_Malloc(STACK_DELTA*sizeof(DataStackEntry)); - if (self->data_stack == NULL) { + if (DataStack_init(self, &self->data_stack)) { + return RET_ERROR; + } + self->data_stack_index = PyDict_New(); + if (self->data_stack_index == NULL) { STATS( self->stats.errors++; ) - PyErr_NoMemory(); return RET_ERROR; } - self->data_stack_alloc = STACK_DELTA; - self->cur_file_data = NULL; - self->last_line = -1; + self->data_stacks = NULL; + self->data_stacks_alloc = 0; + self->data_stacks_used = 0; + + self->pdata_stack = &self->data_stack; + + self->cur_entry.file_data = NULL; + self->cur_entry.last_line = -1; self->last_exc_back = NULL; @@ -165,16 +226,28 @@ CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused) static void CTracer_dealloc(CTracer *self) { + int i; + if (self->started) { PyEval_SetTrace(NULL, NULL); } Py_XDECREF(self->should_trace); Py_XDECREF(self->warn); + Py_XDECREF(self->coroutine_id_func); Py_XDECREF(self->data); + Py_XDECREF(self->plugin_data); Py_XDECREF(self->should_trace_cache); - PyMem_Free(self->data_stack); + DataStack_dealloc(self, &self->data_stack); + if (self->data_stacks) { + for (i = 0; i < self->data_stacks_used; i++) { + DataStack_dealloc(self, self->data_stacks + i); + } + PyMem_Free(self->data_stacks); + } + + Py_XDECREF(self->data_stack_index); Py_TYPE(self)->tp_free((PyObject*)self); } @@ -229,7 +302,7 @@ showlog(int depth, int lineno, PyObject * filename, const char * msg) static const char * what_sym[] = {"CALL", "EXC ", "LINE", "RET "}; #endif -/* Record a pair of integers in self->cur_file_data. */ +/* Record a pair of integers in self->cur_entry.file_data. */ static int CTracer_record_pair(CTracer *self, int l1, int l2) { @@ -237,7 +310,7 @@ CTracer_record_pair(CTracer *self, int l1, int l2) PyObject * t = Py_BuildValue("(ii)", l1, l2); if (t != NULL) { - if (PyDict_SetItem(self->cur_file_data, t, Py_None) < 0) { + if (PyDict_SetItem(self->cur_entry.file_data, t, Py_None) < 0) { STATS( self->stats.errors++; ) ret = RET_ERROR; } @@ -250,6 +323,63 @@ CTracer_record_pair(CTracer *self, int l1, int l2) return ret; } +/* Set self->pdata_stack to the proper data_stack to use. */ +static int +CTracer_set_pdata_stack(CTracer *self) +{ + if (self->coroutine_id_func != Py_None) { + PyObject * co_obj = NULL; + PyObject * stack_index = NULL; + long the_index = 0; + + co_obj = PyObject_CallObject(self->coroutine_id_func, NULL); + if (co_obj == NULL) { + return RET_ERROR; + } + stack_index = PyDict_GetItem(self->data_stack_index, co_obj); + if (stack_index == NULL) { + /* A new coroutine object. Make a new data stack. */ + the_index = self->data_stacks_used; + stack_index = MyInt_FromLong(the_index); + if (PyDict_SetItem(self->data_stack_index, co_obj, stack_index) < 0) { + STATS( self->stats.errors++; ) + Py_XDECREF(co_obj); + Py_XDECREF(stack_index); + return RET_ERROR; + } + self->data_stacks_used++; + if (self->data_stacks_used >= self->data_stacks_alloc) { + int bigger = self->data_stacks_alloc + 10; + DataStack * bigger_stacks = PyMem_Realloc(self->data_stacks, bigger * sizeof(DataStack)); + if (bigger_stacks == NULL) { + STATS( self->stats.errors++; ) + PyErr_NoMemory(); + Py_XDECREF(co_obj); + Py_XDECREF(stack_index); + return RET_ERROR; + } + self->data_stacks = bigger_stacks; + self->data_stacks_alloc = bigger; + } + DataStack_init(self, &self->data_stacks[the_index]); + } + else { + Py_INCREF(stack_index); + the_index = MyInt_AsLong(stack_index); + } + + self->pdata_stack = &self->data_stacks[the_index]; + + Py_XDECREF(co_obj); + Py_XDECREF(stack_index); + } + else { + self->pdata_stack = &self->data_stack; + } + + return RET_OK; +} + /* * The Trace Function */ @@ -260,6 +390,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse PyObject * filename = NULL; PyObject * tracename = NULL; PyObject * disposition = NULL; + PyObject * disp_trace = NULL; #if WHAT_LOG || TRACE_LOG PyObject * ascii = NULL; #endif @@ -294,16 +425,18 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse we'll need to keep more of the missed frame's state. */ STATS( self->stats.missed_returns++; ) - if (self->depth >= 0) { - if (self->tracing_arcs && self->cur_file_data) { - if (CTracer_record_pair(self, self->last_line, -self->last_exc_firstlineno) < 0) { + if (CTracer_set_pdata_stack(self)) { + return RET_ERROR; + } + if (self->pdata_stack->depth >= 0) { + if (self->tracing_arcs && self->cur_entry.file_data) { + if (CTracer_record_pair(self, self->cur_entry.last_line, -self->last_exc_firstlineno) < 0) { return RET_ERROR; } } - SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn"); - self->cur_file_data = self->data_stack[self->depth].file_data; - self->last_line = self->data_stack[self->depth].last_line; - self->depth--; + SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn"); + self->cur_entry = self->pdata_stack->stack[self->pdata_stack->depth]; + self->pdata_stack->depth--; } } self->last_exc_back = NULL; @@ -314,25 +447,15 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse case PyTrace_CALL: /* 0 */ STATS( self->stats.calls++; ) /* Grow the stack. */ - self->depth++; - if (self->depth >= self->data_stack_alloc) { - STATS( self->stats.stack_reallocs++; ) - /* We've outgrown our data_stack array: make it bigger. */ - int bigger = self->data_stack_alloc + STACK_DELTA; - DataStackEntry * bigger_data_stack = PyMem_Realloc(self->data_stack, bigger * sizeof(DataStackEntry)); - if (bigger_data_stack == NULL) { - STATS( self->stats.errors++; ) - PyErr_NoMemory(); - self->depth--; - return RET_ERROR; - } - self->data_stack = bigger_data_stack; - self->data_stack_alloc = bigger; + if (CTracer_set_pdata_stack(self)) { + return RET_ERROR; + } + if (DataStack_grow(self, self->pdata_stack)) { + return RET_ERROR; } /* Push the current state on the stack. */ - self->data_stack[self->depth].file_data = self->cur_file_data; - self->data_stack[self->depth].last_line = self->last_line; + self->pdata_stack->stack[self->pdata_stack->depth] = self->cur_entry; /* Check if we should trace this line. */ filename = frame->f_code->co_filename; @@ -358,15 +481,33 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse Py_INCREF(disposition); } - /* If tracename is a string, then we're supposed to trace. */ - tracename = PyObject_GetAttrString(disposition, "filename"); - if (tracename == NULL) { + disp_trace = PyObject_GetAttrString(disposition, "trace"); + if (disp_trace == NULL) { STATS( self->stats.errors++; ) Py_DECREF(disposition); return RET_ERROR; } + + tracename = Py_None; + Py_INCREF(tracename); + + if (disp_trace == Py_True) { + /* If tracename is a string, then we're supposed to trace. */ + tracename = PyObject_GetAttrString(disposition, "source_filename"); + if (tracename == NULL) { + STATS( self->stats.errors++; ) + Py_DECREF(disposition); + Py_DECREF(disp_trace); + return RET_ERROR; + } + } + Py_DECREF(disp_trace); + if (MyText_Check(tracename)) { PyObject * file_data = PyDict_GetItem(self->data, tracename); + PyObject * disp_plugin = NULL; + PyObject * disp_plugin_name = NULL; + if (file_data == NULL) { file_data = PyDict_New(); if (file_data == NULL) { @@ -383,51 +524,81 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse Py_DECREF(disposition); return RET_ERROR; } + + if (self->plugin_data != NULL) { + /* If the disposition mentions a plugin, record that. */ + disp_plugin = PyObject_GetAttrString(disposition, "plugin"); + if (disp_plugin == NULL) { + STATS( self->stats.errors++; ) + Py_DECREF(tracename); + Py_DECREF(disposition); + return RET_ERROR; + } + if (disp_plugin != Py_None) { + disp_plugin_name = PyObject_GetAttrString(disp_plugin, "__name__"); + Py_DECREF(disp_plugin); + if (disp_plugin_name == NULL) { + STATS( self->stats.errors++; ) + Py_DECREF(tracename); + Py_DECREF(disposition); + return RET_ERROR; + } + ret = PyDict_SetItem(self->plugin_data, tracename, disp_plugin_name); + Py_DECREF(disp_plugin_name); + if (ret < 0) { + Py_DECREF(tracename); + Py_DECREF(disposition); + return RET_ERROR; + } + } + } } - self->cur_file_data = file_data; + self->cur_entry.file_data = file_data; /* Make the frame right in case settrace(gettrace()) happens. */ Py_INCREF(self); frame->f_trace = (PyObject*)self; - SHOWLOG(self->depth, frame->f_lineno, filename, "traced"); + SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "traced"); } else { - self->cur_file_data = NULL; - SHOWLOG(self->depth, frame->f_lineno, filename, "skipped"); + self->cur_entry.file_data = NULL; + SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "skipped"); } Py_DECREF(tracename); Py_DECREF(disposition); - self->last_line = -1; + self->cur_entry.last_line = -1; break; case PyTrace_RETURN: /* 3 */ STATS( self->stats.returns++; ) /* A near-copy of this code is above in the missing-return handler. */ - if (self->depth >= 0) { - if (self->tracing_arcs && self->cur_file_data) { + if (CTracer_set_pdata_stack(self)) { + return RET_ERROR; + } + if (self->pdata_stack->depth >= 0) { + if (self->tracing_arcs && self->cur_entry.file_data) { int first = frame->f_code->co_firstlineno; - if (CTracer_record_pair(self, self->last_line, -first) < 0) { + if (CTracer_record_pair(self, self->cur_entry.last_line, -first) < 0) { return RET_ERROR; } } - SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "return"); - self->cur_file_data = self->data_stack[self->depth].file_data; - self->last_line = self->data_stack[self->depth].last_line; - self->depth--; + SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "return"); + self->cur_entry = self->pdata_stack->stack[self->pdata_stack->depth]; + self->pdata_stack->depth--; } break; case PyTrace_LINE: /* 2 */ STATS( self->stats.lines++; ) - if (self->depth >= 0) { - SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "line"); - if (self->cur_file_data) { + if (self->pdata_stack->depth >= 0) { + SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "line"); + if (self->cur_entry.file_data) { /* We're tracing in this frame: record something. */ if (self->tracing_arcs) { /* Tracing arcs: key is (last_line,this_line). */ - if (CTracer_record_pair(self, self->last_line, frame->f_lineno) < 0) { + if (CTracer_record_pair(self, self->cur_entry.last_line, frame->f_lineno) < 0) { return RET_ERROR; } } @@ -438,7 +609,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse STATS( self->stats.errors++; ) return RET_ERROR; } - ret = PyDict_SetItem(self->cur_file_data, this_line, Py_None); + ret = PyDict_SetItem(self->cur_entry.file_data, this_line, Py_None); Py_DECREF(this_line); if (ret < 0) { STATS( self->stats.errors++; ) @@ -446,7 +617,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse } } } - self->last_line = frame->f_lineno; + self->cur_entry.last_line = frame->f_lineno; } break; @@ -562,7 +733,7 @@ CTracer_start(CTracer *self, PyObject *args_unused) PyEval_SetTrace((Py_tracefunc)CTracer_trace, (PyObject*)self); self->started = 1; self->tracing_arcs = self->arcs && PyObject_IsTrue(self->arcs); - self->last_line = -1; + self->cur_entry.last_line = -1; /* start() returns a trace function usable with sys.settrace() */ Py_INCREF(self); @@ -594,7 +765,7 @@ CTracer_get_stats(CTracer *self) "new_files", self->stats.new_files, "missed_returns", self->stats.missed_returns, "stack_reallocs", self->stats.stack_reallocs, - "stack_alloc", self->data_stack_alloc, + "stack_alloc", self->pdata_stack->alloc, "errors", self->stats.errors ); #else @@ -610,9 +781,15 @@ CTracer_members[] = { { "warn", T_OBJECT, offsetof(CTracer, warn), 0, PyDoc_STR("Function for issuing warnings.") }, + { "coroutine_id_func", T_OBJECT, offsetof(CTracer, coroutine_id_func), 0, + PyDoc_STR("Function for determining coroutine context") }, + { "data", T_OBJECT, offsetof(CTracer, data), 0, PyDoc_STR("The raw dictionary of trace data.") }, + { "plugin_data", T_OBJECT, offsetof(CTracer, plugin_data), 0, + PyDoc_STR("Mapping from filename to plugin name.") }, + { "should_trace_cache", T_OBJECT, offsetof(CTracer, should_trace_cache), 0, PyDoc_STR("Dictionary caching should_trace results.") }, diff --git a/doc/api.rst b/doc/api.rst index a43de17f..0a00947e 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -9,16 +9,17 @@ Coverage API :history: 20100221T151500, docs for 3.3 (on the plane back from PyCon) :history: 20100725T211700, updated for 3.4. :history: 20121111T235800, added a bit of clarification. +:history: 20140819T132600, change class name to Coverage The API to coverage.py is very simple, contained in a single module called -`coverage`. Most of the interface is in a single class, also called -`coverage`. Methods on the coverage object correspond roughly to operations +`coverage`. Most of the interface is in a single class, called +`Coverage`. Methods on the Coverage object correspond roughly to operations available in the command line interface. For example, a simple use would be:: import coverage - cov = coverage.coverage() + cov = coverage.Coverage() cov.start() # .. call your code .. @@ -34,7 +35,7 @@ The coverage module .. module:: coverage -.. autoclass:: coverage +.. autoclass:: Coverage :members: @@ -45,6 +45,9 @@ def run_tests(tracer, *nose_args): import nose.core if tracer == "py": label = "with Python tracer" + if os.environ.get("COVERAGE_NO_PYTRACER"): + print("Skipping tests, don't want PyTracer") + return else: label = "with C tracer" if os.environ.get("COVERAGE_NO_EXTENSION"): @@ -63,9 +66,11 @@ def run_tests_with_coverage(tracer, *nose_args): os.environ['COVERAGE_HOME'] = os.getcwd() # Create the .pth file that will let us measure coverage in sub-processes. + # The .pth file seems to have to be alphabetically after easy-install.pth + # or the sys.path entries aren't created right? import nose pth_dir = os.path.dirname(os.path.dirname(nose.__file__)) - pth_path = os.path.join(pth_dir, "covcov.pth") + pth_path = os.path.join(pth_dir, "zzz_metacov.pth") with open(pth_path, "w") as pth_file: pth_file.write("import coverage; coverage.process_startup()\n") @@ -77,6 +82,7 @@ def run_tests_with_coverage(tracer, *nose_args): # if we clobber the cover_prefix in the coverage object, we can defeat the # self-detection. cov.cover_prefix = "Please measure coverage.py!" + cov._warn_unimported_source = False cov.erase() cov.start() @@ -219,7 +225,8 @@ def print_banner(label): pypy_version = sys.pypy_version_info # pylint: disable=E1101 version += " (pypy %s)" % ".".join(str(v) for v in pypy_version) - print('=== %s %s %s (%s) ===' % (impl, version, label, sys.executable)) + which_python = os.path.relpath(sys.executable) + print('=== %s %s %s (%s) ===' % (impl, version, label, which_python)) def do_help(): diff --git a/metacov.ini b/metacov.ini index 57285d6f..f94b834c 100644 --- a/metacov.ini +++ b/metacov.ini @@ -1,7 +1,7 @@ # Settings to use when using coverage.py to measure itself. [run] branch = true -data_file = $COVERAGE_HOME/.coverage.meta +data_file = $COVERAGE_HOME/.metacov parallel = true source = $COVERAGE_HOME/coverage @@ -36,15 +36,7 @@ precision = 1 [paths] source = - /home/ned/coverage/trunk - /home/ned/coverage/trunk/.tox/py26/lib/python2.6/site-packages - /home/ned/coverage/trunk/.tox/py27/lib/python2.7/site-packages - /home/ned/coverage/trunk/.tox/py32/lib/python3.2/site-packages - /home/ned/coverage/trunk/.tox/py33/lib/python3.3/site-packages - /home/ned/coverage/trunk/.tox/py34/lib/python3.4/site-packages - /home/ned/coverage/trunk/.tox/pypy/site-packages - C:\ned\coverage\trunk - C:\ned\coverage\trunk\.tox\py26\Lib\site-packages - C:\ned\coverage\trunk\.tox\py27\Lib\site-packages - C:\ned\coverage\trunk\.tox\py32\Lib\site-packages - C:\ned\coverage\trunk\.tox\py34\Lib\site-packages + . + */.tox/*/lib/*/site-packages + */.tox/pypy/site-packages + */coverage/trunk @@ -68,6 +68,8 @@ disable= # R0201: 42:Tracer.stop: Method could be a function # E1103: 26:RunTests.test_run_python_file: Instance of 'file' has no 'getvalue' member (but some types could not be inferred) R0201,E1103, +# formatting stuff + superfluous-parens,bad-continuation, # Messages that are noisy for now, eventually maybe we'll turn them on: # C0103:256:coverage.morf_filename: Invalid name "f" (should match [a-z_][a-z0-9_]{2,30}$) # W0212: 86:Reporter.report_files: Access to a protected member _analyze of a client class @@ -79,9 +81,6 @@ disable= # (visual studio) and html output-format=text -# Include message's id in output -include-ids=yes - # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". @@ -245,7 +244,7 @@ max-branches=50 max-statements=150 # Maximum number of parents for a class (see R0901). -max-parents=7 +max-parents=12 # Maximum number of attributes for a class (see R0902). max-attributes=40 diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 1eedad39..4053059f 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -1,48 +1,39 @@ """Base test case class for coverage testing.""" -import glob, os, random, re, shlex, shutil, sys, tempfile, textwrap -import atexit, collections +import glob, os, random, re, shlex, shutil, sys import coverage -from coverage.backward import StringIO, to_bytes, import_local_file +from coverage.backunittest import TestCase +from coverage.backward import StringIO, 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 - -class Tee(object): - """A file-like that writes to all the file-likes it has.""" - - def __init__(self, *files): - """Make a Tee that writes to all the files in `files.`""" - self._files = files - if hasattr(files[0], "encoding"): - self.encoding = files[0].encoding - - def write(self, data): - """Write `data` to all the files.""" - for f in self._files: - f.write(data) +from coverage.test_helpers import ( + ModuleAwareMixin, SysPathAwareMixin, EnvironmentAwareMixin, + StdStreamCapturingMixin, TempDirMixin, +) - if 0: - # Use this if you need to use a debugger, though it makes some tests - # fail, I'm not sure why... - def __getattr__(self, name): - return getattr(self._files[0], name) +from tests.backtest import run_command # Status returns for the command line. OK, ERR = 0, 1 -class CoverageTest(TestCase): +class CoverageTest( + ModuleAwareMixin, + SysPathAwareMixin, + EnvironmentAwareMixin, + StdStreamCapturingMixin, + TempDirMixin, + TestCase +): """A base class for Coverage test cases.""" - # Our own setting: most CoverageTests run in their own temp directory. - run_in_temp_dir = True - # Standard unittest setting: show me diffs even if they are very long. maxDiff = None + # Tell newer unittest implementations to print long helpful messages. + longMessage = True + def setUp(self): super(CoverageTest, self).setUp() @@ -51,153 +42,6 @@ class CoverageTest(TestCase): f.write("%s_%s" % (self.__class__.__name__, self._testMethodName)) f.close() - # Tell newer unittest implementations to print long helpful messages. - self.longMessage = True - - # tearDown will restore the original sys.path - self.old_syspath = sys.path[:] - - if self.run_in_temp_dir: - # Create a temporary directory. - self.noise = str(random.random())[2:] - self.temp_root = os.path.join(tempfile.gettempdir(), 'test_cover') - self.temp_dir = os.path.join(self.temp_root, self.noise) - os.makedirs(self.temp_dir) - self.old_dir = os.getcwd() - os.chdir(self.temp_dir) - - # Modules should be importable from this temp directory. We don't - # use '' because we make lots of different temp directories and - # nose's caching importer can get confused. The full path prevents - # problems. - sys.path.insert(0, os.getcwd()) - - # Keep a counter to make every call to check_coverage unique. - self.n = 0 - - # Record environment variables that we changed with set_environ. - self.environ_undos = {} - - # Capture stdout and stderr so we can examine them in tests. - # nose keeps stdout from littering the screen, so we can safely Tee it, - # but it doesn't capture stderr, so we don't want to Tee stderr to the - # real stderr, since it will interfere with our nice field of dots. - self.old_stdout = sys.stdout - self.captured_stdout = StringIO() - sys.stdout = Tee(sys.stdout, self.captured_stdout) - self.old_stderr = sys.stderr - self.captured_stderr = StringIO() - sys.stderr = self.captured_stderr - - # Record sys.modules here so we can restore it in tearDown. - self.old_modules = dict(sys.modules) - - class_behavior = self.class_behavior() - class_behavior.tests += 1 - class_behavior.test_method_made_any_files = False - class_behavior.temp_dir = self.run_in_temp_dir - - def tearDown(self): - super(CoverageTest, self).tearDown() - - # Restore the original sys.path. - sys.path = self.old_syspath - - if self.run_in_temp_dir: - # Get rid of the temporary directory. - os.chdir(self.old_dir) - shutil.rmtree(self.temp_root) - - # Restore the environment. - self.undo_environ() - - # Restore stdout and stderr - sys.stdout = self.old_stdout - sys.stderr = self.old_stderr - - self.clean_modules() - - class_behavior = self.class_behavior() - if class_behavior.test_method_made_any_files: - class_behavior.tests_making_files += 1 - - def clean_modules(self): - """Remove any new modules imported during the test run. - - This lets us import the same source files for more than one test. - - """ - for m in [m for m in sys.modules if m not in self.old_modules]: - del sys.modules[m] - - def set_environ(self, name, value): - """Set an environment variable `name` to be `value`. - - The environment variable is set, and record is kept that it was set, - so that `tearDown` can restore its original value. - - """ - if name not in self.environ_undos: - self.environ_undos[name] = os.environ.get(name) - os.environ[name] = value - - def original_environ(self, name, if_missing=None): - """The environment variable `name` from when the test started.""" - if name in self.environ_undos: - ret = self.environ_undos[name] - else: - ret = os.environ.get(name) - if ret is None: - ret = if_missing - return ret - - def undo_environ(self): - """Undo all the changes made by `set_environ`.""" - for name, value in self.environ_undos.items(): - if value is None: - del os.environ[name] - else: - os.environ[name] = value - - def stdout(self): - """Return the data written to stdout during the test.""" - return self.captured_stdout.getvalue() - - def stderr(self): - """Return the data written to stderr during the test.""" - return self.captured_stderr.getvalue() - - def make_file(self, filename, text="", newline=None): - """Create a file for testing. - - `filename` is the relative path to the file, including directories if - desired, which will be created if need be. `text` is the content to - create in the file. If `newline` is provided, it is a string that will - be used as the line endings in the created file, otherwise the line - endings are as provided in `text`. - - Returns `filename`. - - """ - # Tests that call `make_file` should be run in a temp environment. - assert self.run_in_temp_dir - self.class_behavior().test_method_made_any_files = True - - text = textwrap.dedent(text) - if newline: - text = text.replace("\n", newline) - - # Make sure the directories are available. - dirs, _ = os.path.split(filename) - if dirs and not os.path.exists(dirs): - os.makedirs(dirs) - - # Create the file. - with open(filename, 'wb') as f: - f.write(to_bytes(text)) - - return filename - def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. @@ -206,7 +50,7 @@ class CoverageTest(TestCase): """ # So that we can re-import files, clean them out first. - self.clean_modules() + self.cleanup_modules() # Also have to clean out the .pyc file, since the timestamp # resolution is only one second, a changed file might not be # picked up. @@ -244,13 +88,7 @@ class CoverageTest(TestCase): def get_module_name(self): """Return the module name to use for this test run.""" - # We append self.n because otherwise two calls in one test will use the - # same filename and whether the test works or not depends on the - # timestamps in the .pyc file, so it becomes random whether the second - # call will use the compiled version of the first call's code or not! - modname = 'coverage_test_' + self.noise + str(self.n) - self.n += 1 - return modname + return 'coverage_test_' + str(random.random())[2:] # Map chars to numbers for arcz_to_arcs _arcz_map = {'.': -1} @@ -507,55 +345,3 @@ class CoverageTest(TestCase): 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 - # option properly, so we track information about what the tests did, and - # then report at the end of the process on test classes that were set - # wrong. - - class ClassBehavior(object): - """A value object to store per-class in CoverageTest.""" - def __init__(self): - self.tests = 0 - self.temp_dir = True - self.tests_making_files = 0 - self.test_method_made_any_files = False - - # Map from class to info about how it ran. - class_behaviors = collections.defaultdict(ClassBehavior) - - @classmethod - def report_on_class_behavior(cls): - """Called at process exit to report on class behavior.""" - for test_class, behavior in cls.class_behaviors.items(): - if behavior.temp_dir and behavior.tests_making_files == 0: - bad = "Inefficient" - elif not behavior.temp_dir and behavior.tests_making_files > 0: - bad = "Unsafe" - else: - bad = "" - - if bad: - if behavior.temp_dir: - where = "in a temp directory" - else: - where = "without a temp directory" - print( - "%s: %s ran %d tests, %d made files %s" % ( - bad, - test_class.__name__, - behavior.tests, - behavior.tests_making_files, - where, - ) - ) - - def class_behavior(self): - """Get the ClassBehavior instance for this test.""" - return self.class_behaviors[self.__class__] - - -# When the process ends, find out about bad classes. -atexit.register(CoverageTest.report_on_class_behavior) diff --git a/tests/farm/html/run_unicode.py b/tests/farm/html/run_unicode.py index cef26ee5..c8cb6c50 100644 --- a/tests/farm/html/run_unicode.py +++ b/tests/farm/html/run_unicode.py @@ -1,5 +1,3 @@ -import sys - def html_it(): """Run coverage and make an HTML report for unicode.py.""" import coverage @@ -18,13 +16,9 @@ contains("html_unicode/unicode.html", "<span class='str'>"ʎd˙ǝbɐɹǝʌoɔ"</span>", ) -if sys.maxunicode == 65535: - contains("html_unicode/unicode.html", - "<span class='str'>"db40,dd00: x��"</span>", - ) -else: - contains("html_unicode/unicode.html", - "<span class='str'>"db40,dd00: x󠄀"</span>", - ) +contains_any("html_unicode/unicode.html", + "<span class='str'>"db40,dd00: x��"</span>", + "<span class='str'>"db40,dd00: x󠄀"</span>", + ) clean("html_unicode") diff --git a/tests/modules/plugins/__init__.py b/tests/modules/plugins/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/modules/plugins/__init__.py diff --git a/tests/modules/plugins/a_plugin.py b/tests/modules/plugins/a_plugin.py new file mode 100644 index 00000000..2ff84dac --- /dev/null +++ b/tests/modules/plugins/a_plugin.py @@ -0,0 +1,6 @@ +"""A plugin for tests to reference.""" + +from coverage import CoveragePlugin + +class Plugin(CoveragePlugin): + pass diff --git a/tests/modules/plugins/another.py b/tests/modules/plugins/another.py new file mode 100644 index 00000000..2ff84dac --- /dev/null +++ b/tests/modules/plugins/another.py @@ -0,0 +1,6 @@ +"""A plugin for tests to reference.""" + +from coverage import CoveragePlugin + +class Plugin(CoveragePlugin): + pass diff --git a/tests/test_backward.py b/tests/test_backward.py index 2c688edd..09803ba7 100644 --- a/tests/test_backward.py +++ b/tests/test_backward.py @@ -1,14 +1,12 @@ """Tests that our version shims in backward.py are working.""" +from coverage.backunittest import TestCase from coverage.backward import iitems, binary_bytes, byte_to_int, bytes_to_ints -from tests.backunittest import TestCase class BackwardTest(TestCase): """Tests of things from backward.py.""" - run_in_temp_dir = False - def test_iitems(self): d = {'a': 1, 'b': 2, 'c': 3} items = [('a', 1), ('b', 2), ('c', 3)] diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 038e9214..08f7937a 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -72,7 +72,7 @@ class CmdLineTest(CoverageTest): code = re.sub(r"(?m)^\.", "m2.", code) m2 = self.model_object() code_obj = compile(code, "<code>", "exec") - eval(code_obj, globals(), { 'm2': m2 }) + eval(code_obj, globals(), { 'm2': m2 }) # pylint: disable=eval-used # Many of our functions take a lot of arguments, and cmdline.py # calls them with many. But most of them are just the defaults, which diff --git a/tests/test_config.py b/tests/test_config.py index 7409f4aa..bf84423d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- """Test the config file handling for coverage.py""" +import sys, os + import coverage from coverage.misc import CoverageException @@ -125,6 +127,12 @@ class ConfigTest(CoverageTest): class ConfigFileTest(CoverageTest): """Tests of the config file settings in particular.""" + def setUp(self): + super(ConfigFileTest, self).setUp() + # Parent class saves and restores sys.path, we can just modify it. + # Add modules to the path so we can import plugins. + sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules')) + # This sample file tries to use lots of variation of syntax... # The {section} placeholder lets us nest these settings in another file. LOTSA_SETTINGS = """\ @@ -136,6 +144,9 @@ class ConfigFileTest(CoverageTest): cover_pylib = TRUE parallel = on include = a/ , b/ + plugins = + plugins.a_plugin + plugins.another [{section}report] ; these settings affect reporting. @@ -174,6 +185,10 @@ class ConfigFileTest(CoverageTest): other = other, /home/ned/other, c:\\Ned\\etc + [{section}plugins.a_plugin] + hello = world + ; comments still work. + names = Jane/John/Jenny """ # Just some sample setup.cfg text from the docs. @@ -212,6 +227,9 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.partial_always_list, ["if 0:", "while True:"] ) + self.assertEqual(cov.config.plugins, + ["plugins.a_plugin", "plugins.another"] + ) self.assertTrue(cov.config.show_missing) self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere") self.assertEqual(cov.config.extra_css, "something/extra.css") @@ -224,6 +242,12 @@ class ConfigFileTest(CoverageTest): 'other': ['other', '/home/ned/other', 'c:\\Ned\\etc'] }) + self.assertEqual(cov.config.get_plugin_options("plugins.a_plugin"), { + 'hello': 'world', + 'names': 'Jane/John/Jenny', + }) + self.assertEqual(cov.config.get_plugin_options("plugins.another"), {}) + def test_config_file_settings(self): self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) cov = coverage.coverage() diff --git a/tests/test_coroutine.py b/tests/test_coroutine.py index fe6c8326..4abdd6f6 100644 --- a/tests/test_coroutine.py +++ b/tests/test_coroutine.py @@ -1,8 +1,7 @@ """Tests for coroutining.""" -import os.path, sys +import os, os.path, sys, threading -from nose.plugins.skip import SkipTest import coverage from tests.coveragetest import CoverageTest @@ -20,6 +19,14 @@ try: except ImportError: gevent = None +try: + import greenlet # pylint: disable=import-error +except ImportError: + greenlet = None + +# Are we running with the C tracer or not? +C_TRACER = os.getenv('COVERAGE_TEST_TRACER', 'c') == 'c' + def line_count(s): """How many non-blank non-comment lines are in `s`?""" @@ -96,48 +103,106 @@ class CoroutineTest(CoverageTest): import gevent.queue as queue """ + COMMON - def try_some_code(self, code, args): - """Run some coroutine testing code and see that it was all covered.""" + # Uncomplicated code that doesn't use any of the coroutining stuff, to test + # the simple case under each of the regimes. + SIMPLE = """\ + total = 0 + for i in range({LIMIT}): + total += i + print(total) + """.format(LIMIT=LIMIT) - self.make_file("try_it.py", code) + def try_some_code(self, code, coroutine, the_module, expected_out=None): + """Run some coroutine testing code and see that it was all covered. - 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) + `code` is the Python code to execute. `coroutine` is the name of the + coroutine regime to test it under. `the_module` is the imported module + that must be available for this to work at all. `expected_out` is the + text we expect the code to produce. - # 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) + self.make_file("try_it.py", code) - lines = line_count(code) - self.assertEqual(data.summary()['try_it.py'], lines) + cmd = "coverage run --coroutine=%s try_it.py" % coroutine + out = self.run_command(cmd) + + if not the_module: + # We don't even have the underlying module installed, we expect + # coverage to alert us to this fact. + expected_out = ( + "Couldn't trace with coroutine=%s, " + "the module isn't installed.\n" % coroutine + ) + self.assertEqual(out, expected_out) + elif C_TRACER or coroutine == "thread": + # We can fully measure the code if we are using the C tracer, which + # can support all the coroutining, or if we are using threads. + if expected_out is None: + 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) + else: + expected_out = ( + "Can't support coroutine=%s with PyTracer, " + "only threads are supported\n" % coroutine + ) + self.assertEqual(out, expected_out) def test_threads(self): - self.try_some_code(self.THREAD, "") + self.try_some_code(self.THREAD, "thread", threading) + + def test_threads_simple_code(self): + self.try_some_code(self.SIMPLE, "thread", threading) def test_eventlet(self): - if eventlet is None: - raise SkipTest("No eventlet available") + self.try_some_code(self.EVENTLET, "eventlet", eventlet) - self.try_some_code(self.EVENTLET, "--coroutine=eventlet") + def test_eventlet_simple_code(self): + self.try_some_code(self.SIMPLE, "eventlet", eventlet) def test_gevent(self): - raise SkipTest("Still not sure why gevent isn't working...") + self.try_some_code(self.GEVENT, "gevent", gevent) + + def test_gevent_simple_code(self): + self.try_some_code(self.SIMPLE, "gevent", gevent) + + def test_greenlet(self): + GREENLET = """\ + from greenlet import greenlet + + def test1(x, y): + z = gr2.switch(x+y) + print(z) + + def test2(u): + print(u) + gr1.switch(42) - if gevent is None: - raise SkipTest("No gevent available") + gr1 = greenlet(test1) + gr2 = greenlet(test2) + gr1.switch("hello", " world") + """ + self.try_some_code(GREENLET, "greenlet", greenlet, "hello world\n42\n") - self.try_some_code(self.GEVENT, "--coroutine=gevent") + def test_greenlet_simple_code(self): + self.try_some_code(self.SIMPLE, "greenlet", greenlet) 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)) + print(" {0} {1}".format("X" if lineno in linenos else " ", line)) diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 2427847e..69616e84 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -16,7 +16,7 @@ class RunFileTest(CoverageTest): def test_run_python_file(self): tryfile = os.path.join(here, "try_execfile.py") run_python_file(tryfile, [tryfile, "arg1", "arg2"]) - mod_globs = eval(self.stdout()) + mod_globs = eval(self.stdout()) # pylint: disable=eval-used # The file should think it is __main__ self.assertEqual(mod_globs['__name__'], "__main__") diff --git a/tests/test_farm.py b/tests/test_farm.py index b2ea3697..47f9b7b7 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -79,7 +79,8 @@ class FarmTestCase(object): # Prepare a dictionary of globals for the run.py files to use. fns = """ - copy run runfunc compare contains doesnt_contain clean skip + copy run runfunc clean skip + compare contains contains_any doesnt_contain """.split() if self.clean_only: glo = dict((fn, self.noop) for fn in fns) @@ -304,6 +305,22 @@ class FarmTestCase(object): for s in strlist: assert s in text, "Missing content in %s: %r" % (filename, s) + def contains_any(self, filename, *strlist): + """Check that the file contains at least one of a list of strings. + + An assert will be raised if none of the arguments in `strlist` is in + `filename`. + + """ + with open(filename, "r") as fobj: + text = fobj.read() + for s in strlist: + if s in text: + return + assert False, "Missing content in %s: %r [1 of %d]" % ( + filename, strlist[0], len(strlist), + ) + def doesnt_contain(self, filename, *strlist): """Check that the file contains none of a list of strings. diff --git a/tests/test_files.py b/tests/test_files.py index 070430ff..648c76a9 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -99,6 +99,14 @@ class MatcherTest(CoverageTest): self.assertMatches(fnm, "x123foo.txt", True) self.assertMatches(fnm, "x798bar.txt", False) + def test_fnmatch_windows_paths(self): + # We should be able to match Windows paths even if we are running on + # a non-Windows OS. + fnm = FnmatchMatcher(["*/foo.py"]) + self.assertMatches(fnm, r"dir\foo.py", True) + fnm = FnmatchMatcher([r"*\foo.py"]) + self.assertMatches(fnm, r"dir\foo.py", True) + class PathAliasesTest(CoverageTest): """Tests for coverage/files.py:PathAliases""" diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 786ede94..47f492f6 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -116,9 +116,8 @@ class RecursionTest(CoverageTest): self.assertEqual(statements, [1,2,3,5,7,8,9,10,11]) self.assertEqual(missing, expected_missing) - # We can get a warning about the stackoverflow effect on the tracing - # function only if we have sys.gettrace - if pytrace and hasattr(sys, "gettrace"): + # Get a warning about the stackoverflow effect on the tracing function. + if pytrace: self.assertEqual(cov._warnings, ["Trace function changed, measurement is likely wrong: None"] ) @@ -368,35 +367,34 @@ class DoctestTest(CoverageTest): [1,11,12,14,16,17], "") -if hasattr(sys, 'gettrace'): - class GettraceTest(CoverageTest): - """Tests that we work properly with `sys.gettrace()`.""" - def test_round_trip(self): - self.check_coverage('''\ - import sys - def foo(n): - return 3*n - def bar(n): - return 5*n - a = foo(6) +class GettraceTest(CoverageTest): + """Tests that we work properly with `sys.gettrace()`.""" + def test_round_trip(self): + self.check_coverage('''\ + import sys + def foo(n): + return 3*n + def bar(n): + return 5*n + a = foo(6) + sys.settrace(sys.gettrace()) + a = bar(8) + ''', + [1,2,3,4,5,6,7,8], "") + + def test_multi_layers(self): + self.check_coverage('''\ + import sys + def level1(): + a = 3 + level2() + b = 5 + def level2(): + c = 7 sys.settrace(sys.gettrace()) - a = bar(8) - ''', - [1,2,3,4,5,6,7,8], "") - - def test_multi_layers(self): - self.check_coverage('''\ - import sys - def level1(): - a = 3 - level2() - b = 5 - def level2(): - c = 7 - sys.settrace(sys.gettrace()) - d = 9 - e = 10 - level1() - f = 12 - ''', - [1,2,3,4,5,6,7,8,9,10,11,12], "") + d = 9 + e = 10 + level1() + f = 12 + ''', + [1,2,3,4,5,6,7,8,9,10,11,12], "") diff --git a/tests/test_plugins.py b/tests/test_plugins.py new file mode 100644 index 00000000..9c5a037d --- /dev/null +++ b/tests/test_plugins.py @@ -0,0 +1,217 @@ +"""Tests for plugins.""" + +import os.path + +import coverage +from coverage.codeunit import CodeUnit +from coverage.parser import CodeParser +from coverage.plugin import Plugins, overrides + +from tests.coveragetest import CoverageTest + + +class FakeConfig(object): + """A fake config for use in tests.""" + + def __init__(self, plugin, options): + self.plugin = plugin + self.options = options + self.asked_for = [] + + def get_plugin_options(self, module): + """Just return the options for `module` if this is the right module.""" + self.asked_for.append(module) + if module == self.plugin: + return self.options + else: + return {} + + +class PluginUnitTest(CoverageTest): + """Test Plugins.load_plugins directly.""" + + def test_importing_and_configuring(self): + self.make_file("plugin1.py", """\ + from coverage import CoveragePlugin + + class Plugin(CoveragePlugin): + def __init__(self, options): + super(Plugin, self).__init__(options) + self.this_is = "me" + """) + + config = FakeConfig("plugin1", {'a':'hello'}) + plugins = list(Plugins.load_plugins(["plugin1"], config)) + + self.assertEqual(len(plugins), 1) + self.assertEqual(plugins[0].this_is, "me") + self.assertEqual(plugins[0].options, {'a':'hello'}) + self.assertEqual(config.asked_for, ['plugin1']) + + def test_importing_and_configuring_more_than_one(self): + self.make_file("plugin1.py", """\ + from coverage import CoveragePlugin + + class Plugin(CoveragePlugin): + def __init__(self, options): + super(Plugin, self).__init__(options) + self.this_is = "me" + """) + self.make_file("plugin2.py", """\ + from coverage import CoveragePlugin + + class Plugin(CoveragePlugin): + pass + """) + + config = FakeConfig("plugin1", {'a':'hello'}) + plugins = list(Plugins.load_plugins(["plugin1", "plugin2"], config)) + + self.assertEqual(len(plugins), 2) + self.assertEqual(plugins[0].this_is, "me") + self.assertEqual(plugins[0].options, {'a':'hello'}) + self.assertEqual(plugins[1].options, {}) + self.assertEqual(config.asked_for, ['plugin1', 'plugin2']) + + def test_cant_import(self): + with self.assertRaises(ImportError): + _ = Plugins.load_plugins(["plugin_not_there"], None) + + def test_ok_to_not_define_plugin(self): + self.make_file("plugin2.py", """\ + from coverage import CoveragePlugin + + Nothing = 0 + """) + plugins = list(Plugins.load_plugins(["plugin2"], None)) + self.assertEqual(plugins, []) + + +class PluginTest(CoverageTest): + """Test plugins through the Coverage class.""" + + def test_plugin_imported(self): + # Prove that a plugin will be imported. + self.make_file("my_plugin.py", """\ + with open("evidence.out", "w") as f: + f.write("we are here!") + """) + + self.assert_doesnt_exist("evidence.out") + _ = coverage.Coverage(plugins=["my_plugin"]) + + with open("evidence.out") as f: + self.assertEqual(f.read(), "we are here!") + + def test_missing_plugin_raises_import_error(self): + # Prove that a missing plugin will raise an ImportError. + with self.assertRaises(ImportError): + cov = coverage.Coverage(plugins=["does_not_exist_woijwoicweo"]) + cov.start() + + def test_bad_plugin_isnt_hidden(self): + # Prove that a plugin with an error in it will raise the error. + self.make_file("plugin_over_zero.py", """\ + 1/0 + """) + with self.assertRaises(ZeroDivisionError): + _ = coverage.Coverage(plugins=["plugin_over_zero"]) + + def test_importing_myself(self): + self.make_file("simple.py", """\ + import try_xyz + a = 1 + b = 2 + """) + self.make_file("try_xyz.py", """\ + c = 3 + d = 4 + """) + + cov = coverage.Coverage(plugins=["tests.test_plugins"]) + + # Import the python file, executing it. + self.start_import_stop(cov, "simple") + + _, statements, missing, _ = cov.analysis("simple.py") + self.assertEqual(statements, [1,2,3]) + self.assertEqual(missing, []) + _, statements, _, _ = cov.analysis("/src/try_ABC.zz") + self.assertEqual(statements, [105, 106, 107, 205, 206, 207]) + + +class Plugin(coverage.CoveragePlugin): + def trace_judge(self, disp): + if "xyz.py" in disp.original_filename: + disp.trace = True + disp.source_filename = os.path.join( + "/src", + os.path.basename( + disp.original_filename.replace("xyz.py", "ABC.zz") + ) + ) + + def line_number_range(self, frame): + lineno = frame.f_lineno + return lineno*100+5, lineno*100+7 + + def code_unit_class(self, filename): + return PluginCodeUnit + +class PluginCodeUnit(CodeUnit): + def get_parser(self, exclude=None): + return PluginParser() + +class PluginParser(CodeParser): + def parse_source(self): + return set([105, 106, 107, 205, 206, 207]), set([]) + + +class OverridesTest(CoverageTest): + """Test plugins.py:overrides.""" + + run_in_temp_dir = False + + def test_overrides(self): + class SomeBase(object): + """Base class, two base methods.""" + def method1(self): + pass + + def method2(self): + pass + + class Derived1(SomeBase): + """Simple single inheritance.""" + def method1(self): + pass + + self.assertTrue(overrides(Derived1(), "method1", SomeBase)) + self.assertFalse(overrides(Derived1(), "method2", SomeBase)) + + class FurtherDerived1(Derived1): + """Derive again from Derived1, inherit its method1.""" + pass + + self.assertTrue(overrides(FurtherDerived1(), "method1", SomeBase)) + self.assertFalse(overrides(FurtherDerived1(), "method2", SomeBase)) + + class FurtherDerived2(Derived1): + """Override the overridden method.""" + def method1(self): + pass + + self.assertTrue(overrides(FurtherDerived2(), "method1", SomeBase)) + self.assertFalse(overrides(FurtherDerived2(), "method2", SomeBase)) + + class Mixin(object): + """A mixin that overrides method1.""" + def method1(self): + pass + + class Derived2(Mixin, SomeBase): + """A class that gets the method from the mixin.""" + pass + + self.assertTrue(overrides(Derived2(), "method1", SomeBase)) + self.assertFalse(overrides(Derived2(), "method2", SomeBase)) diff --git a/tests/test_process.py b/tests/test_process.py index d8314982..3a0980dc 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -470,8 +470,7 @@ class ProcessTest(CoverageTest): self.assertIn("Hello\n", out) self.assertIn("Goodbye\n", out) - if hasattr(sys, "gettrace"): - self.assertIn("Trace function changed", out) + self.assertIn("Trace function changed", out) if sys.version_info >= (3, 0): # This only works on 3.x for now. # It only works with the C tracer, diff --git a/tests/test_testing.py b/tests/test_testing.py index 049a1982..4a19098f 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -2,16 +2,14 @@ """Tests that our test infrastructure is really working!""" import os, sys +from coverage.backunittest import TestCase from coverage.backward import to_bytes -from tests.backunittest import TestCase -from tests.coveragetest import CoverageTest +from tests.coveragetest import TempDirMixin, CoverageTest class TestingTest(TestCase): """Tests of helper methods on `backunittest.TestCase`.""" - run_in_temp_dir = False - def test_assert_count_equal(self): self.assertCountEqual(set(), set()) self.assertCountEqual(set([1,2,3]), set([3,1,2])) @@ -21,26 +19,27 @@ class TestingTest(TestCase): self.assertCountEqual(set([1,2,3]), set([4,5,6])) -class CoverageTestTest(CoverageTest): - """Test the methods in `CoverageTest`.""" +class TempDirMixinTest(TempDirMixin, TestCase): + """Test the methods in TempDirMixin.""" def file_text(self, fname): """Return the text read from a file.""" - return open(fname, "rb").read().decode('ascii') + with open(fname, "rb") as f: + return f.read().decode('ascii') def test_make_file(self): # A simple file. self.make_file("fooey.boo", "Hello there") - self.assertEqual(open("fooey.boo").read(), "Hello there") + self.assertEqual(self.file_text("fooey.boo"), "Hello there") # A file in a sub-directory self.make_file("sub/another.txt", "Another") - self.assertEqual(open("sub/another.txt").read(), "Another") + self.assertEqual(self.file_text("sub/another.txt"), "Another") # A second file in that sub-directory self.make_file("sub/second.txt", "Second") - self.assertEqual(open("sub/second.txt").read(), "Second") + self.assertEqual(self.file_text("sub/second.txt"), "Second") # A deeper directory self.make_file("sub/deeper/evenmore/third.txt") - self.assertEqual(open("sub/deeper/evenmore/third.txt").read(), "") + self.assertEqual(self.file_text("sub/deeper/evenmore/third.txt"), "") def test_make_file_newline(self): self.make_file("unix.txt", "Hello\n") @@ -52,10 +51,13 @@ class CoverageTestTest(CoverageTest): def test_make_file_non_ascii(self): self.make_file("unicode.txt", "tabblo: «ταБЬℓσ»") - self.assertEqual( - open("unicode.txt", "rb").read(), - to_bytes("tabblo: «ταБЬℓσ»") - ) + with open("unicode.txt", "rb") as f: + text = f.read() + self.assertEqual(text, to_bytes("tabblo: «ταБЬℓσ»")) + + +class CoverageTestTest(CoverageTest): + """Test the methods in `CoverageTest`.""" def test_file_exists(self): self.make_file("whoville.txt", "We are here!") @@ -28,18 +28,33 @@ deps = [testenv:py26] deps = - nose - mock + {[testenv]deps} unittest2 gevent eventlet + greenlet [testenv:py27] deps = - nose - mock + {[testenv]deps} gevent eventlet + greenlet + +[testenv:py32] +deps = + {[testenv]deps} + greenlet + +[testenv:py33] +deps = + {[testenv]deps} + greenlet + +[testenv:py34] +deps = + {[testenv]deps} + greenlet [testenv:pypy] # PyPy has no C extensions |