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