sbc | 0cec9d7 | 2014-11-24 17:25:29 | [diff] [blame^] | 1 | """Helper for building, testing, and linting coverage.py. |
| 2 | |
| 3 | To get portability, all these operations are written in Python here instead |
| 4 | of in shell scripts, batch files, or Makefiles. |
| 5 | |
| 6 | """ |
| 7 | |
| 8 | import fnmatch |
| 9 | import glob |
| 10 | import inspect |
| 11 | import os |
| 12 | import platform |
| 13 | import socket |
| 14 | import sys |
| 15 | import zipfile |
| 16 | |
| 17 | |
| 18 | # Functions named do_* are executable from the command line: do_blah is run |
| 19 | # by "python igor.py blah". |
| 20 | |
| 21 | |
| 22 | def do_remove_extension(): |
| 23 | """Remove the compiled C extension, no matter what its name.""" |
| 24 | |
| 25 | so_patterns = """ |
| 26 | tracer.so |
| 27 | tracer.*.so |
| 28 | tracer.pyd |
| 29 | """.split() |
| 30 | |
| 31 | for pattern in so_patterns: |
| 32 | pattern = os.path.join("coverage", pattern) |
| 33 | for filename in glob.glob(pattern): |
| 34 | try: |
| 35 | os.remove(filename) |
| 36 | except OSError: |
| 37 | pass |
| 38 | |
| 39 | def run_tests(tracer, *nose_args): |
| 40 | """The actual running of tests.""" |
| 41 | import nose.core |
| 42 | if tracer == "py": |
| 43 | label = "with Python tracer" |
| 44 | else: |
| 45 | label = "with C tracer" |
| 46 | if os.environ.get("COVERAGE_NO_EXTENSION"): |
| 47 | print("Skipping tests, no C extension in this environment") |
| 48 | return |
| 49 | print_banner(label) |
| 50 | os.environ["COVERAGE_TEST_TRACER"] = tracer |
| 51 | nose_args = ["nosetests"] + list(nose_args) |
| 52 | nose.core.main(argv=nose_args) |
| 53 | |
| 54 | def run_tests_with_coverage(tracer, *nose_args): |
| 55 | """Run tests, but with coverage.""" |
| 56 | import coverage |
| 57 | |
| 58 | os.environ['COVERAGE_PROCESS_START'] = os.path.abspath('metacov.ini') |
| 59 | os.environ['COVERAGE_HOME'] = os.getcwd() |
| 60 | |
| 61 | # Create the .pth file that will let us measure coverage in sub-processes. |
| 62 | import nose |
| 63 | pth_dir = os.path.dirname(os.path.dirname(nose.__file__)) |
| 64 | pth_path = os.path.join(pth_dir, "covcov.pth") |
| 65 | pth_file = open(pth_path, "w") |
| 66 | try: |
| 67 | pth_file.write("import coverage; coverage.process_startup()\n") |
| 68 | finally: |
| 69 | pth_file.close() |
| 70 | |
| 71 | version = "%s%s" % sys.version_info[:2] |
| 72 | suffix = "%s_%s_%s" % (version, tracer, socket.gethostname()) |
| 73 | |
| 74 | cov = coverage.coverage(config_file="metacov.ini", data_suffix=suffix) |
| 75 | # Cheap trick: the coverage code itself is excluded from measurement, but |
| 76 | # if we clobber the cover_prefix in the coverage object, we can defeat the |
| 77 | # self-detection. |
| 78 | cov.cover_prefix = "Please measure coverage.py!" |
| 79 | cov.erase() |
| 80 | cov.start() |
| 81 | |
| 82 | try: |
| 83 | # Re-import coverage to get it coverage tested! I don't understand all |
| 84 | # the mechanics here, but if I don't carry over the imported modules |
| 85 | # (in covmods), then things go haywire (os == None, eventually). |
| 86 | covmods = {} |
| 87 | covdir = os.path.split(coverage.__file__)[0] |
| 88 | # We have to make a list since we'll be deleting in the loop. |
| 89 | modules = list(sys.modules.items()) |
| 90 | for name, mod in modules: |
| 91 | if name.startswith('coverage'): |
| 92 | if getattr(mod, '__file__', "??").startswith(covdir): |
| 93 | covmods[name] = mod |
| 94 | del sys.modules[name] |
| 95 | import coverage # don't warn about re-import: pylint: disable=W0404 |
| 96 | sys.modules.update(covmods) |
| 97 | |
| 98 | # Run nosetests, with the arguments from our command line. |
| 99 | try: |
| 100 | run_tests(tracer, *nose_args) |
| 101 | except SystemExit: |
| 102 | # nose3 seems to raise SystemExit, not sure why? |
| 103 | pass |
| 104 | finally: |
| 105 | cov.stop() |
| 106 | os.remove(pth_path) |
| 107 | |
| 108 | cov.save() |
| 109 | |
| 110 | def do_combine_html(): |
| 111 | """Combine data from a meta-coverage run, and make the HTML report.""" |
| 112 | import coverage |
| 113 | os.environ['COVERAGE_HOME'] = os.getcwd() |
| 114 | cov = coverage.coverage(config_file="metacov.ini") |
| 115 | cov.load() |
| 116 | cov.combine() |
| 117 | cov.save() |
| 118 | cov.html_report() |
| 119 | |
| 120 | def do_test_with_tracer(tracer, *noseargs): |
| 121 | """Run nosetests with a particular tracer.""" |
| 122 | if os.environ.get("COVERAGE_COVERAGE", ""): |
| 123 | return run_tests_with_coverage(tracer, *noseargs) |
| 124 | else: |
| 125 | return run_tests(tracer, *noseargs) |
| 126 | |
| 127 | def do_zip_mods(): |
| 128 | """Build the zipmods.zip file.""" |
| 129 | zf = zipfile.ZipFile("tests/zipmods.zip", "w") |
| 130 | zf.write("tests/covmodzip1.py", "covmodzip1.py") |
| 131 | zf.close() |
| 132 | |
| 133 | def do_install_egg(): |
| 134 | """Install the egg1 egg for tests.""" |
| 135 | # I am pretty certain there are easier ways to install eggs... |
| 136 | # pylint: disable=F0401,E0611,E1101 |
| 137 | import distutils.core |
| 138 | cur_dir = os.getcwd() |
| 139 | os.chdir("tests/eggsrc") |
| 140 | distutils.core.run_setup("setup.py", ["--quiet", "bdist_egg"]) |
| 141 | egg = glob.glob("dist/*.egg")[0] |
| 142 | distutils.core.run_setup( |
| 143 | "setup.py", ["--quiet", "easy_install", "--no-deps", "--zip-ok", egg] |
| 144 | ) |
| 145 | os.chdir(cur_dir) |
| 146 | |
| 147 | def do_check_eol(): |
| 148 | """Check files for incorrect newlines and trailing whitespace.""" |
| 149 | |
| 150 | ignore_dirs = [ |
| 151 | '.svn', '.hg', '.tox', '.tox_kits', 'coverage.egg-info', |
| 152 | '_build', 'covtestegg1.egg-info', |
| 153 | ] |
| 154 | checked = set([]) |
| 155 | |
| 156 | def check_file(fname, crlf=True, trail_white=True): |
| 157 | """Check a single file for whitespace abuse.""" |
| 158 | fname = os.path.relpath(fname) |
| 159 | if fname in checked: |
| 160 | return |
| 161 | checked.add(fname) |
| 162 | |
| 163 | line = None |
| 164 | for n, line in enumerate(open(fname, "rb")): |
| 165 | if crlf: |
| 166 | if "\r" in line: |
| 167 | print("%s@%d: CR found" % (fname, n+1)) |
| 168 | return |
| 169 | if trail_white: |
| 170 | line = line[:-1] |
| 171 | if not crlf: |
| 172 | line = line.rstrip('\r') |
| 173 | if line.rstrip() != line: |
| 174 | print("%s@%d: trailing whitespace found" % (fname, n+1)) |
| 175 | return |
| 176 | |
| 177 | if line is not None and not line.strip(): |
| 178 | print("%s: final blank line" % (fname,)) |
| 179 | |
| 180 | def check_files(root, patterns, **kwargs): |
| 181 | """Check a number of files for whitespace abuse.""" |
| 182 | for root, dirs, files in os.walk(root): |
| 183 | for f in files: |
| 184 | fname = os.path.join(root, f) |
| 185 | for p in patterns: |
| 186 | if fnmatch.fnmatch(fname, p): |
| 187 | check_file(fname, **kwargs) |
| 188 | break |
| 189 | for dir_name in ignore_dirs: |
| 190 | if dir_name in dirs: |
| 191 | dirs.remove(dir_name) |
| 192 | |
| 193 | check_files("coverage", ["*.py", "*.c"]) |
| 194 | check_files("coverage/htmlfiles", ["*.html", "*.css", "*.js"]) |
| 195 | check_file("tests/farm/html/src/bom.py", crlf=False) |
| 196 | check_files("tests", ["*.py"]) |
| 197 | check_files("tests", ["*,cover"], trail_white=False) |
| 198 | check_files("tests/js", ["*.js", "*.html"]) |
| 199 | check_file("setup.py") |
| 200 | check_file("igor.py") |
| 201 | check_file("Makefile") |
| 202 | check_file(".hgignore") |
| 203 | check_file(".travis.yml") |
| 204 | check_files("doc", ["*.rst"]) |
| 205 | check_files(".", ["*.txt"]) |
| 206 | |
| 207 | |
| 208 | def print_banner(label): |
| 209 | """Print the version of Python.""" |
| 210 | try: |
| 211 | impl = platform.python_implementation() |
| 212 | except AttributeError: |
| 213 | impl = "Python" |
| 214 | |
| 215 | version = platform.python_version() |
| 216 | |
| 217 | if '__pypy__' in sys.builtin_module_names: |
| 218 | pypy_version = sys.pypy_version_info # pylint: disable=E1101 |
| 219 | version += " (pypy %s)" % ".".join([str(v) for v in pypy_version]) |
| 220 | |
| 221 | print('=== %s %s %s (%s) ===' % (impl, version, label, sys.executable)) |
| 222 | |
| 223 | |
| 224 | def do_help(): |
| 225 | """List the available commands""" |
| 226 | items = list(globals().items()) |
| 227 | items.sort() |
| 228 | for name, value in items: |
| 229 | if name.startswith('do_'): |
| 230 | print("%-20s%s" % (name[3:], value.__doc__)) |
| 231 | |
| 232 | |
| 233 | def main(args): |
| 234 | """Main command-line execution for igor. |
| 235 | |
| 236 | Verbs are taken from the command line, and extra words taken as directed |
| 237 | by the arguments needed by the handler. |
| 238 | |
| 239 | """ |
| 240 | while args: |
| 241 | verb = args.pop(0) |
| 242 | handler = globals().get('do_'+verb) |
| 243 | if handler is None: |
| 244 | print("*** No handler for %r" % verb) |
| 245 | return 1 |
| 246 | argspec = inspect.getargspec(handler) |
| 247 | if argspec[1]: |
| 248 | # Handler has *args, give it all the rest of the command line. |
| 249 | handler_args = args |
| 250 | args = [] |
| 251 | else: |
| 252 | # Handler has specific arguments, give it only what it needs. |
| 253 | num_args = len(argspec[0]) |
| 254 | handler_args = args[:num_args] |
| 255 | args = args[num_args:] |
| 256 | ret = handler(*handler_args) |
| 257 | # If a handler returns a failure-like value, stop. |
| 258 | if ret: |
| 259 | return ret |
| 260 | |
| 261 | if __name__ == '__main__': |
| 262 | sys.exit(main(sys.argv[1:])) |