Home | History | Annotate | Download | only in coverage
      1 # coding: utf-8
      2 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
      3 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
      4 
      5 """Helper for building, testing, and linting coverage.py.
      6 
      7 To get portability, all these operations are written in Python here instead
      8 of in shell scripts, batch files, or Makefiles.
      9 
     10 """
     11 
     12 import contextlib
     13 import fnmatch
     14 import glob
     15 import inspect
     16 import os
     17 import platform
     18 import sys
     19 import textwrap
     20 import warnings
     21 import zipfile
     22 
     23 
     24 # We want to see all warnings while we are running tests.  But we also need to
     25 # disable warnings for some of the more complex setting up of tests.
     26 warnings.simplefilter("default")
     27 
     28 
     29 @contextlib.contextmanager
     30 def ignore_warnings():
     31     """Context manager to ignore warning within the with statement."""
     32     with warnings.catch_warnings():
     33         warnings.simplefilter("ignore")
     34         yield
     35 
     36 
     37 # Functions named do_* are executable from the command line: do_blah is run
     38 # by "python igor.py blah".
     39 
     40 
     41 def do_show_env():
     42     """Show the environment variables."""
     43     print("Environment:")
     44     for env in sorted(os.environ):
     45         print("  %s = %r" % (env, os.environ[env]))
     46 
     47 
     48 def do_remove_extension():
     49     """Remove the compiled C extension, no matter what its name."""
     50 
     51     so_patterns = """
     52         tracer.so
     53         tracer.*.so
     54         tracer.pyd
     55         tracer.*.pyd
     56         """.split()
     57 
     58     for pattern in so_patterns:
     59         pattern = os.path.join("coverage", pattern)
     60         for filename in glob.glob(pattern):
     61             try:
     62                 os.remove(filename)
     63             except OSError:
     64                 pass
     65 
     66 
     67 def label_for_tracer(tracer):
     68     """Get the label for these tests."""
     69     if tracer == "py":
     70         label = "with Python tracer"
     71     else:
     72         label = "with C tracer"
     73 
     74     return label
     75 
     76 
     77 def should_skip(tracer):
     78     """Is there a reason to skip these tests?"""
     79     if tracer == "py":
     80         skipper = os.environ.get("COVERAGE_NO_PYTRACER")
     81     else:
     82         skipper = (
     83             os.environ.get("COVERAGE_NO_EXTENSION") or
     84             os.environ.get("COVERAGE_NO_CTRACER")
     85         )
     86 
     87     if skipper:
     88         msg = "Skipping tests " + label_for_tracer(tracer)
     89         if len(skipper) > 1:
     90             msg += ": " + skipper
     91     else:
     92         msg = ""
     93 
     94     return msg
     95 
     96 
     97 def run_tests(tracer, *nose_args):
     98     """The actual running of tests."""
     99     with ignore_warnings():
    100         import nose.core
    101 
    102     if 'COVERAGE_TESTING' not in os.environ:
    103         os.environ['COVERAGE_TESTING'] = "True"
    104     print_banner(label_for_tracer(tracer))
    105     nose_args = ["nosetests"] + list(nose_args)
    106     nose.core.main(argv=nose_args)
    107 
    108 
    109 def run_tests_with_coverage(tracer, *nose_args):
    110     """Run tests, but with coverage."""
    111 
    112     # Need to define this early enough that the first import of env.py sees it.
    113     os.environ['COVERAGE_TESTING'] = "True"
    114     os.environ['COVERAGE_PROCESS_START'] = os.path.abspath('metacov.ini')
    115     os.environ['COVERAGE_HOME'] = os.getcwd()
    116 
    117     # Create the .pth file that will let us measure coverage in sub-processes.
    118     # The .pth file seems to have to be alphabetically after easy-install.pth
    119     # or the sys.path entries aren't created right?
    120     import nose
    121     pth_dir = os.path.dirname(os.path.dirname(nose.__file__))
    122     pth_path = os.path.join(pth_dir, "zzz_metacov.pth")
    123     with open(pth_path, "w") as pth_file:
    124         pth_file.write("import coverage; coverage.process_startup()\n")
    125 
    126     # Make names for the data files that keep all the test runs distinct.
    127     impl = platform.python_implementation().lower()
    128     version = "%s%s" % sys.version_info[:2]
    129     if '__pypy__' in sys.builtin_module_names:
    130         version += "_%s%s" % sys.pypy_version_info[:2]
    131     suffix = "%s%s_%s_%s" % (impl, version, tracer, platform.platform())
    132 
    133     os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov."+suffix)
    134 
    135     import coverage
    136     cov = coverage.Coverage(config_file="metacov.ini", data_suffix=False)
    137     # Cheap trick: the coverage.py code itself is excluded from measurement,
    138     # but if we clobber the cover_prefix in the coverage object, we can defeat
    139     # the self-detection.
    140     cov.cover_prefix = "Please measure coverage.py!"
    141     cov._warn_unimported_source = False
    142     cov.start()
    143 
    144     try:
    145         # Re-import coverage to get it coverage tested!  I don't understand all
    146         # the mechanics here, but if I don't carry over the imported modules
    147         # (in covmods), then things go haywire (os == None, eventually).
    148         covmods = {}
    149         covdir = os.path.split(coverage.__file__)[0]
    150         # We have to make a list since we'll be deleting in the loop.
    151         modules = list(sys.modules.items())
    152         for name, mod in modules:
    153             if name.startswith('coverage'):
    154                 if getattr(mod, '__file__', "??").startswith(covdir):
    155                     covmods[name] = mod
    156                     del sys.modules[name]
    157         import coverage                         # pylint: disable=reimported
    158         sys.modules.update(covmods)
    159 
    160         # Run nosetests, with the arguments from our command line.
    161         try:
    162             run_tests(tracer, *nose_args)
    163         except SystemExit:
    164             # nose3 seems to raise SystemExit, not sure why?
    165             pass
    166     finally:
    167         cov.stop()
    168         os.remove(pth_path)
    169 
    170     cov.combine()
    171     cov.save()
    172 
    173 
    174 def do_combine_html():
    175     """Combine data from a meta-coverage run, and make the HTML and XML reports."""
    176     import coverage
    177     os.environ['COVERAGE_HOME'] = os.getcwd()
    178     os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov")
    179     cov = coverage.Coverage(config_file="metacov.ini")
    180     cov.load()
    181     cov.combine()
    182     cov.save()
    183     cov.html_report()
    184     cov.xml_report()
    185 
    186 
    187 def do_test_with_tracer(tracer, *noseargs):
    188     """Run nosetests with a particular tracer."""
    189     # If we should skip these tests, skip them.
    190     skip_msg = should_skip(tracer)
    191     if skip_msg:
    192         print(skip_msg)
    193         return
    194 
    195     os.environ["COVERAGE_TEST_TRACER"] = tracer
    196     if os.environ.get("COVERAGE_COVERAGE", ""):
    197         return run_tests_with_coverage(tracer, *noseargs)
    198     else:
    199         return run_tests(tracer, *noseargs)
    200 
    201 
    202 def do_zip_mods():
    203     """Build the zipmods.zip file."""
    204     zf = zipfile.ZipFile("tests/zipmods.zip", "w")
    205 
    206     # Take one file from disk.
    207     zf.write("tests/covmodzip1.py", "covmodzip1.py")
    208 
    209     # The others will be various encodings.
    210     source = textwrap.dedent(u"""\
    211         # coding: {encoding}
    212         text = u"{text}"
    213         ords = {ords}
    214         assert [ord(c) for c in text] == ords
    215         print(u"All OK with {encoding}")
    216         """)
    217     # These encodings should match the list in tests/test_python.py
    218     details = [
    219         (u'utf8', u', '),
    220         (u'gb2312', u''),
    221         (u'hebrew', u', '),
    222         (u'shift_jis', u''),
    223         (u'cp1252', u'hi'),
    224     ]
    225     for encoding, text in details:
    226         filename = 'encoded_{0}.py'.format(encoding)
    227         ords = [ord(c) for c in text]
    228         source_text = source.format(encoding=encoding, text=text, ords=ords)
    229         zf.writestr(filename, source_text.encode(encoding))
    230 
    231     zf.close()
    232 
    233 
    234 def do_install_egg():
    235     """Install the egg1 egg for tests."""
    236     # I am pretty certain there are easier ways to install eggs...
    237     # pylint: disable=import-error,no-name-in-module
    238     cur_dir = os.getcwd()
    239     os.chdir("tests/eggsrc")
    240     with ignore_warnings():
    241         import distutils.core
    242         distutils.core.run_setup("setup.py", ["--quiet", "bdist_egg"])
    243         egg = glob.glob("dist/*.egg")[0]
    244         distutils.core.run_setup(
    245             "setup.py", ["--quiet", "easy_install", "--no-deps", "--zip-ok", egg]
    246         )
    247     os.chdir(cur_dir)
    248 
    249 
    250 def do_check_eol():
    251     """Check files for incorrect newlines and trailing whitespace."""
    252 
    253     ignore_dirs = [
    254         '.svn', '.hg', '.git',
    255         '.tox*',
    256         '*.egg-info',
    257         '_build',
    258     ]
    259     checked = set()
    260 
    261     def check_file(fname, crlf=True, trail_white=True):
    262         """Check a single file for whitespace abuse."""
    263         fname = os.path.relpath(fname)
    264         if fname in checked:
    265             return
    266         checked.add(fname)
    267 
    268         line = None
    269         with open(fname, "rb") as f:
    270             for n, line in enumerate(f, start=1):
    271                 if crlf:
    272                     if "\r" in line:
    273                         print("%s@%d: CR found" % (fname, n))
    274                         return
    275                 if trail_white:
    276                     line = line[:-1]
    277                     if not crlf:
    278                         line = line.rstrip('\r')
    279                     if line.rstrip() != line:
    280                         print("%s@%d: trailing whitespace found" % (fname, n))
    281                         return
    282 
    283         if line is not None and not line.strip():
    284             print("%s: final blank line" % (fname,))
    285 
    286     def check_files(root, patterns, **kwargs):
    287         """Check a number of files for whitespace abuse."""
    288         for root, dirs, files in os.walk(root):
    289             for f in files:
    290                 fname = os.path.join(root, f)
    291                 for p in patterns:
    292                     if fnmatch.fnmatch(fname, p):
    293                         check_file(fname, **kwargs)
    294                         break
    295             for ignore_dir in ignore_dirs:
    296                 ignored = []
    297                 for dir_name in dirs:
    298                     if fnmatch.fnmatch(dir_name, ignore_dir):
    299                         ignored.append(dir_name)
    300                 for dir_name in ignored:
    301                     dirs.remove(dir_name)
    302 
    303     check_files("coverage", ["*.py"])
    304     check_files("coverage/ctracer", ["*.c", "*.h"])
    305     check_files("coverage/htmlfiles", ["*.html", "*.css", "*.js"])
    306     check_file("tests/farm/html/src/bom.py", crlf=False)
    307     check_files("tests", ["*.py"])
    308     check_files("tests", ["*,cover"], trail_white=False)
    309     check_files("tests/js", ["*.js", "*.html"])
    310     check_file("setup.py")
    311     check_file("igor.py")
    312     check_file("Makefile")
    313     check_file(".hgignore")
    314     check_file(".travis.yml")
    315     check_files(".", ["*.rst", "*.txt"])
    316     check_files(".", ["*.pip"])
    317 
    318 
    319 def print_banner(label):
    320     """Print the version of Python."""
    321     try:
    322         impl = platform.python_implementation()
    323     except AttributeError:
    324         impl = "Python"
    325 
    326     version = platform.python_version()
    327 
    328     if '__pypy__' in sys.builtin_module_names:
    329         version += " (pypy %s)" % ".".join(str(v) for v in sys.pypy_version_info)
    330 
    331     which_python = os.path.relpath(sys.executable)
    332     print('=== %s %s %s (%s) ===' % (impl, version, label, which_python))
    333     sys.stdout.flush()
    334 
    335 
    336 def do_help():
    337     """List the available commands"""
    338     items = list(globals().items())
    339     items.sort()
    340     for name, value in items:
    341         if name.startswith('do_'):
    342             print("%-20s%s" % (name[3:], value.__doc__))
    343 
    344 
    345 def analyze_args(function):
    346     """What kind of args does `function` expect?
    347 
    348     Returns:
    349         star, num_pos:
    350             star(boolean): Does `function` accept *args?
    351             num_args(int): How many positional arguments does `function` have?
    352     """
    353     try:
    354         getargspec = inspect.getfullargspec
    355     except AttributeError:
    356         getargspec = inspect.getargspec
    357     argspec = getargspec(function)
    358     return bool(argspec[1]), len(argspec[0])
    359 
    360 
    361 def main(args):
    362     """Main command-line execution for igor.
    363 
    364     Verbs are taken from the command line, and extra words taken as directed
    365     by the arguments needed by the handler.
    366 
    367     """
    368     while args:
    369         verb = args.pop(0)
    370         handler = globals().get('do_'+verb)
    371         if handler is None:
    372             print("*** No handler for %r" % verb)
    373             return 1
    374         star, num_args = analyze_args(handler)
    375         if star:
    376             # Handler has *args, give it all the rest of the command line.
    377             handler_args = args
    378             args = []
    379         else:
    380             # Handler has specific arguments, give it only what it needs.
    381             handler_args = args[:num_args]
    382             args = args[num_args:]
    383         ret = handler(*handler_args)
    384         # If a handler returns a failure-like value, stop.
    385         if ret:
    386             return ret
    387 
    388 
    389 if __name__ == '__main__':
    390     sys.exit(main(sys.argv[1:]))
    391