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
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
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.
26warnings.simplefilter("default")
27
28
29@contextlib.contextmanager
30def 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
41def 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
48def 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
67def 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
77def 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
97def 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
109def 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
174def 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
187def 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
202def 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
234def 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
250def 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
319def 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
336def 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
345def 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
361def 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
389if __name__ == '__main__':
390    sys.exit(main(sys.argv[1:]))
391