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