blob: 12ec6c8fec4afe1fe482cbcbcda18759135ff60e [file] [log] [blame]
sbc0cec9d72014-11-24 17:25:291"""Helper for building, testing, and linting coverage.py.
2
3To get portability, all these operations are written in Python here instead
4of in shell scripts, batch files, or Makefiles.
5
6"""
7
8import fnmatch
9import glob
10import inspect
11import os
12import platform
13import socket
14import sys
15import 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
22def 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
39def 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
54def 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
110def 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
120def 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
127def 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
133def 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
147def 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
208def 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
224def 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
233def 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
261if __name__ == '__main__':
262 sys.exit(main(sys.argv[1:]))