Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Side by Side Diff: third_party/coverage-3.6/coverage/control.py

Issue 14988009: First cut of testing infrastructure for recipes. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: nitfixen Created 7 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/coverage-3.6/coverage/config.py ('k') | third_party/coverage-3.6/coverage/data.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 """Core control stuff for Coverage."""
2
3 import atexit, os, random, socket, sys
4
5 from coverage.annotate import AnnotateReporter
6 from coverage.backward import string_class, iitems
7 from coverage.codeunit import code_unit_factory, CodeUnit
8 from coverage.collector import Collector
9 from coverage.config import CoverageConfig
10 from coverage.data import CoverageData
11 from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
12 from coverage.files import PathAliases, find_python_files, prep_patterns
13 from coverage.html import HtmlReporter
14 from coverage.misc import CoverageException, bool_or_none, join_regex
15 from coverage.misc import file_be_gone
16 from coverage.results import Analysis, Numbers
17 from coverage.summary import SummaryReporter
18 from coverage.xmlreport import XmlReporter
19
20 class coverage(object):
21 """Programmatic access to coverage.py.
22
23 To use::
24
25 from coverage import coverage
26
27 cov = coverage()
28 cov.start()
29 #.. call your code ..
30 cov.stop()
31 cov.html_report(directory='covhtml')
32
33 """
34 def __init__(self, data_file=None, data_suffix=None, cover_pylib=None,
35 auto_data=False, timid=None, branch=None, config_file=True,
36 source=None, omit=None, include=None):
37 """
38 `data_file` is the base name of the data file to use, defaulting to
39 ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
40 create the final file name. If `data_suffix` is simply True, then a
41 suffix is created with the machine and process identity included.
42
43 `cover_pylib` is a boolean determining whether Python code installed
44 with the Python interpreter is measured. This includes the Python
45 standard library and any packages installed with the interpreter.
46
47 If `auto_data` is true, then any existing data file will be read when
48 coverage measurement starts, and data will be saved automatically when
49 measurement stops.
50
51 If `timid` is true, then a slower and simpler trace function will be
52 used. This is important for some environments where manipulation of
53 tracing functions breaks the faster trace function.
54
55 If `branch` is true, then branch coverage will be measured in addition
56 to the usual statement coverage.
57
58 `config_file` determines what config file to read. If it is a string,
59 it is the name of the config file to read. If it is True, then a
60 standard file is read (".coveragerc"). If it is False, then no file is
61 read.
62
63 `source` is a list of file paths or package names. Only code located
64 in the trees indicated by the file paths or package names will be
65 measured.
66
67 `include` and `omit` are lists of filename patterns. Files that match
68 `include` will be measured, files that match `omit` will not. Each
69 will also accept a single string argument.
70
71 """
72 from coverage import __version__
73
74 # A record of all the warnings that have been issued.
75 self._warnings = []
76
77 # Build our configuration from a number of sources:
78 # 1: defaults:
79 self.config = CoverageConfig()
80
81 # 2: from the coveragerc file:
82 if config_file:
83 if config_file is True:
84 config_file = ".coveragerc"
85 try:
86 self.config.from_file(config_file)
87 except ValueError:
88 _, err, _ = sys.exc_info()
89 raise CoverageException(
90 "Couldn't read config file %s: %s" % (config_file, err)
91 )
92
93 # 3: from environment variables:
94 self.config.from_environment('COVERAGE_OPTIONS')
95 env_data_file = os.environ.get('COVERAGE_FILE')
96 if env_data_file:
97 self.config.data_file = env_data_file
98
99 # 4: from constructor arguments:
100 self.config.from_args(
101 data_file=data_file, cover_pylib=cover_pylib, timid=timid,
102 branch=branch, parallel=bool_or_none(data_suffix),
103 source=source, omit=omit, include=include
104 )
105
106 self.auto_data = auto_data
107
108 # _exclude_re is a dict mapping exclusion list names to compiled
109 # regexes.
110 self._exclude_re = {}
111 self._exclude_regex_stale()
112
113 self.file_locator = FileLocator()
114
115 # The source argument can be directories or package names.
116 self.source = []
117 self.source_pkgs = []
118 for src in self.config.source or []:
119 if os.path.exists(src):
120 self.source.append(self.file_locator.canonical_filename(src))
121 else:
122 self.source_pkgs.append(src)
123
124 self.omit = prep_patterns(self.config.omit)
125 self.include = prep_patterns(self.config.include)
126
127 self.collector = Collector(
128 self._should_trace, timid=self.config.timid,
129 branch=self.config.branch, warn=self._warn
130 )
131
132 # Suffixes are a bit tricky. We want to use the data suffix only when
133 # collecting data, not when combining data. So we save it as
134 # `self.run_suffix` now, and promote it to `self.data_suffix` if we
135 # find that we are collecting data later.
136 if data_suffix or self.config.parallel:
137 if not isinstance(data_suffix, string_class):
138 # if data_suffix=True, use .machinename.pid.random
139 data_suffix = True
140 else:
141 data_suffix = None
142 self.data_suffix = None
143 self.run_suffix = data_suffix
144
145 # Create the data file. We do this at construction time so that the
146 # data file will be written into the directory where the process
147 # started rather than wherever the process eventually chdir'd to.
148 self.data = CoverageData(
149 basename=self.config.data_file,
150 collector="coverage v%s" % __version__
151 )
152
153 # The dirs for files considered "installed with the interpreter".
154 self.pylib_dirs = []
155 if not self.config.cover_pylib:
156 # Look at where some standard modules are located. That's the
157 # indication for "installed with the interpreter". In some
158 # environments (virtualenv, for example), these modules may be
159 # spread across a few locations. Look at all the candidate modules
160 # we've imported, and take all the different ones.
161 for m in (atexit, os, random, socket):
162 if hasattr(m, "__file__"):
163 m_dir = self._canonical_dir(m)
164 if m_dir not in self.pylib_dirs:
165 self.pylib_dirs.append(m_dir)
166
167 # To avoid tracing the coverage code itself, we skip anything located
168 # where we are.
169 self.cover_dir = self._canonical_dir(__file__)
170
171 # The matchers for _should_trace, created when tracing starts.
172 self.source_match = None
173 self.pylib_match = self.cover_match = None
174 self.include_match = self.omit_match = None
175
176 # Set the reporting precision.
177 Numbers.set_precision(self.config.precision)
178
179 # Is it ok for no data to be collected?
180 self._warn_no_data = True
181 self._warn_unimported_source = True
182
183 # State machine variables:
184 # Have we started collecting and not stopped it?
185 self._started = False
186 # Have we measured some data and not harvested it?
187 self._measured = False
188
189 atexit.register(self._atexit)
190
191 def _canonical_dir(self, morf):
192 """Return the canonical directory of the module or file `morf`."""
193 return os.path.split(CodeUnit(morf, self.file_locator).filename)[0]
194
195 def _source_for_file(self, filename):
196 """Return the source file for `filename`."""
197 if not filename.endswith(".py"):
198 if filename[-4:-1] == ".py":
199 filename = filename[:-1]
200 elif filename.endswith("$py.class"): # jython
201 filename = filename[:-9] + ".py"
202 return filename
203
204 def _should_trace(self, filename, frame):
205 """Decide whether to trace execution in `filename`
206
207 This function is called from the trace function. As each new file name
208 is encountered, this function determines whether it is traced or not.
209
210 Returns a canonicalized filename if it should be traced, False if it
211 should not.
212
213 """
214 if not filename:
215 # Empty string is pretty useless
216 return False
217
218 if filename.startswith('<'):
219 # Lots of non-file execution is represented with artificial
220 # filenames like "<string>", "<doctest readme.txt[0]>", or
221 # "<exec_function>". Don't ever trace these executions, since we
222 # can't do anything with the data later anyway.
223 return False
224
225 self._check_for_packages()
226
227 # Compiled Python files have two filenames: frame.f_code.co_filename is
228 # the filename at the time the .pyc was compiled. The second name is
229 # __file__, which is where the .pyc was actually loaded from. Since
230 # .pyc files can be moved after compilation (for example, by being
231 # installed), we look for __file__ in the frame and prefer it to the
232 # co_filename value.
233 dunder_file = frame.f_globals.get('__file__')
234 if dunder_file:
235 filename = self._source_for_file(dunder_file)
236
237 # Jython reports the .class file to the tracer, use the source file.
238 if filename.endswith("$py.class"):
239 filename = filename[:-9] + ".py"
240
241 canonical = self.file_locator.canonical_filename(filename)
242
243 # If the user specified source or include, then that's authoritative
244 # about the outer bound of what to measure and we don't have to apply
245 # any canned exclusions. If they didn't, then we have to exclude the
246 # stdlib and coverage.py directories.
247 if self.source_match:
248 if not self.source_match.match(canonical):
249 return False
250 elif self.include_match:
251 if not self.include_match.match(canonical):
252 return False
253 else:
254 # If we aren't supposed to trace installed code, then check if this
255 # is near the Python standard library and skip it if so.
256 if self.pylib_match and self.pylib_match.match(canonical):
257 return False
258
259 # We exclude the coverage code itself, since a little of it will be
260 # measured otherwise.
261 if self.cover_match and self.cover_match.match(canonical):
262 return False
263
264 # Check the file against the omit pattern.
265 if self.omit_match and self.omit_match.match(canonical):
266 return False
267
268 return canonical
269
270 # To log what should_trace returns, change this to "if 1:"
271 if 0:
272 _real_should_trace = _should_trace
273 def _should_trace(self, filename, frame): # pylint: disable=E0102
274 """A logging decorator around the real _should_trace function."""
275 ret = self._real_should_trace(filename, frame)
276 print("should_trace: %r -> %r" % (filename, ret))
277 return ret
278
279 def _warn(self, msg):
280 """Use `msg` as a warning."""
281 self._warnings.append(msg)
282 sys.stderr.write("Coverage.py warning: %s\n" % msg)
283
284 def _check_for_packages(self):
285 """Update the source_match matcher with latest imported packages."""
286 # Our self.source_pkgs attribute is a list of package names we want to
287 # measure. Each time through here, we see if we've imported any of
288 # them yet. If so, we add its file to source_match, and we don't have
289 # to look for that package any more.
290 if self.source_pkgs:
291 found = []
292 for pkg in self.source_pkgs:
293 try:
294 mod = sys.modules[pkg]
295 except KeyError:
296 continue
297
298 found.append(pkg)
299
300 try:
301 pkg_file = mod.__file__
302 except AttributeError:
303 pkg_file = None
304 else:
305 d, f = os.path.split(pkg_file)
306 if f.startswith('__init__'):
307 # This is actually a package, return the directory.
308 pkg_file = d
309 else:
310 pkg_file = self._source_for_file(pkg_file)
311 pkg_file = self.file_locator.canonical_filename(pkg_file)
312 if not os.path.exists(pkg_file):
313 pkg_file = None
314
315 if pkg_file:
316 self.source.append(pkg_file)
317 self.source_match.add(pkg_file)
318 else:
319 self._warn("Module %s has no Python source." % pkg)
320
321 for pkg in found:
322 self.source_pkgs.remove(pkg)
323
324 def use_cache(self, usecache):
325 """Control the use of a data file (incorrectly called a cache).
326
327 `usecache` is true or false, whether to read and write data on disk.
328
329 """
330 self.data.usefile(usecache)
331
332 def load(self):
333 """Load previously-collected coverage data from the data file."""
334 self.collector.reset()
335 self.data.read()
336
337 def start(self):
338 """Start measuring code coverage.
339
340 Coverage measurement actually occurs in functions called after `start`
341 is invoked. Statements in the same scope as `start` won't be measured.
342
343 Once you invoke `start`, you must also call `stop` eventually, or your
344 process might not shut down cleanly.
345
346 """
347 if self.run_suffix:
348 # Calling start() means we're running code, so use the run_suffix
349 # as the data_suffix when we eventually save the data.
350 self.data_suffix = self.run_suffix
351 if self.auto_data:
352 self.load()
353
354 # Create the matchers we need for _should_trace
355 if self.source or self.source_pkgs:
356 self.source_match = TreeMatcher(self.source)
357 else:
358 if self.cover_dir:
359 self.cover_match = TreeMatcher([self.cover_dir])
360 if self.pylib_dirs:
361 self.pylib_match = TreeMatcher(self.pylib_dirs)
362 if self.include:
363 self.include_match = FnmatchMatcher(self.include)
364 if self.omit:
365 self.omit_match = FnmatchMatcher(self.omit)
366
367 self.collector.start()
368 self._started = True
369 self._measured = True
370
371 def stop(self):
372 """Stop measuring code coverage."""
373 self._started = False
374 self.collector.stop()
375
376 def _atexit(self):
377 """Clean up on process shutdown."""
378 if self._started:
379 self.stop()
380 if self.auto_data:
381 self.save()
382
383 def erase(self):
384 """Erase previously-collected coverage data.
385
386 This removes the in-memory data collected in this session as well as
387 discarding the data file.
388
389 """
390 self.collector.reset()
391 self.data.erase()
392
393 def clear_exclude(self, which='exclude'):
394 """Clear the exclude list."""
395 setattr(self.config, which + "_list", [])
396 self._exclude_regex_stale()
397
398 def exclude(self, regex, which='exclude'):
399 """Exclude source lines from execution consideration.
400
401 A number of lists of regular expressions are maintained. Each list
402 selects lines that are treated differently during reporting.
403
404 `which` determines which list is modified. The "exclude" list selects
405 lines that are not considered executable at all. The "partial" list
406 indicates lines with branches that are not taken.
407
408 `regex` is a regular expression. The regex is added to the specified
409 list. If any of the regexes in the list is found in a line, the line
410 is marked for special treatment during reporting.
411
412 """
413 excl_list = getattr(self.config, which + "_list")
414 excl_list.append(regex)
415 self._exclude_regex_stale()
416
417 def _exclude_regex_stale(self):
418 """Drop all the compiled exclusion regexes, a list was modified."""
419 self._exclude_re.clear()
420
421 def _exclude_regex(self, which):
422 """Return a compiled regex for the given exclusion list."""
423 if which not in self._exclude_re:
424 excl_list = getattr(self.config, which + "_list")
425 self._exclude_re[which] = join_regex(excl_list)
426 return self._exclude_re[which]
427
428 def get_exclude_list(self, which='exclude'):
429 """Return a list of excluded regex patterns.
430
431 `which` indicates which list is desired. See `exclude` for the lists
432 that are available, and their meaning.
433
434 """
435 return getattr(self.config, which + "_list")
436
437 def save(self):
438 """Save the collected coverage data to the data file."""
439 data_suffix = self.data_suffix
440 if data_suffix is True:
441 # If data_suffix was a simple true value, then make a suffix with
442 # plenty of distinguishing information. We do this here in
443 # `save()` at the last minute so that the pid will be correct even
444 # if the process forks.
445 extra = ""
446 if _TEST_NAME_FILE:
447 f = open(_TEST_NAME_FILE)
448 test_name = f.read()
449 f.close()
450 extra = "." + test_name
451 data_suffix = "%s%s.%s.%06d" % (
452 socket.gethostname(), extra, os.getpid(),
453 random.randint(0, 999999)
454 )
455
456 self._harvest_data()
457 self.data.write(suffix=data_suffix)
458
459 def combine(self):
460 """Combine together a number of similarly-named coverage data files.
461
462 All coverage data files whose name starts with `data_file` (from the
463 coverage() constructor) will be read, and combined together into the
464 current measurements.
465
466 """
467 aliases = None
468 if self.config.paths:
469 aliases = PathAliases(self.file_locator)
470 for paths in self.config.paths.values():
471 result = paths[0]
472 for pattern in paths[1:]:
473 aliases.add(pattern, result)
474 self.data.combine_parallel_data(aliases=aliases)
475
476 def _harvest_data(self):
477 """Get the collected data and reset the collector.
478
479 Also warn about various problems collecting data.
480
481 """
482 if self._measured:
483 self.data.add_line_data(self.collector.get_line_data())
484 self.data.add_arc_data(self.collector.get_arc_data())
485 self.collector.reset()
486
487 # If there are still entries in the source_pkgs list, then we never
488 # encountered those packages.
489 if self._warn_unimported_source:
490 for pkg in self.source_pkgs:
491 self._warn("Module %s was never imported." % pkg)
492
493 # Find out if we got any data.
494 summary = self.data.summary()
495 if not summary and self._warn_no_data:
496 self._warn("No data was collected.")
497
498 # Find files that were never executed at all.
499 for src in self.source:
500 for py_file in find_python_files(src):
501 py_file = self.file_locator.canonical_filename(py_file)
502 self.data.touch_file(py_file)
503
504 self._measured = False
505
506 # Backward compatibility with version 1.
507 def analysis(self, morf):
508 """Like `analysis2` but doesn't return excluded line numbers."""
509 f, s, _, m, mf = self.analysis2(morf)
510 return f, s, m, mf
511
512 def analysis2(self, morf):
513 """Analyze a module.
514
515 `morf` is a module or a filename. It will be analyzed to determine
516 its coverage statistics. The return value is a 5-tuple:
517
518 * The filename for the module.
519 * A list of line numbers of executable statements.
520 * A list of line numbers of excluded statements.
521 * A list of line numbers of statements not run (missing from
522 execution).
523 * A readable formatted string of the missing line numbers.
524
525 The analysis uses the source file itself and the current measured
526 coverage data.
527
528 """
529 analysis = self._analyze(morf)
530 return (
531 analysis.filename, analysis.statements, analysis.excluded,
532 analysis.missing, analysis.missing_formatted()
533 )
534
535 def _analyze(self, it):
536 """Analyze a single morf or code unit.
537
538 Returns an `Analysis` object.
539
540 """
541 self._harvest_data()
542 if not isinstance(it, CodeUnit):
543 it = code_unit_factory(it, self.file_locator)[0]
544
545 return Analysis(self, it)
546
547 def report(self, morfs=None, show_missing=True, ignore_errors=None,
548 file=None, # pylint: disable=W0622
549 omit=None, include=None
550 ):
551 """Write a summary report to `file`.
552
553 Each module in `morfs` is listed, with counts of statements, executed
554 statements, missing statements, and a list of lines missed.
555
556 `include` is a list of filename patterns. Modules whose filenames
557 match those patterns will be included in the report. Modules matching
558 `omit` will not be included in the report.
559
560 Returns a float, the total percentage covered.
561
562 """
563 self._harvest_data()
564 self.config.from_args(
565 ignore_errors=ignore_errors, omit=omit, include=include,
566 show_missing=show_missing,
567 )
568 reporter = SummaryReporter(self, self.config)
569 return reporter.report(morfs, outfile=file)
570
571 def annotate(self, morfs=None, directory=None, ignore_errors=None,
572 omit=None, include=None):
573 """Annotate a list of modules.
574
575 Each module in `morfs` is annotated. The source is written to a new
576 file, named with a ",cover" suffix, with each line prefixed with a
577 marker to indicate the coverage of the line. Covered lines have ">",
578 excluded lines have "-", and missing lines have "!".
579
580 See `coverage.report()` for other arguments.
581
582 """
583 self._harvest_data()
584 self.config.from_args(
585 ignore_errors=ignore_errors, omit=omit, include=include
586 )
587 reporter = AnnotateReporter(self, self.config)
588 reporter.report(morfs, directory=directory)
589
590 def html_report(self, morfs=None, directory=None, ignore_errors=None,
591 omit=None, include=None, extra_css=None, title=None):
592 """Generate an HTML report.
593
594 The HTML is written to `directory`. The file "index.html" is the
595 overview starting point, with links to more detailed pages for
596 individual modules.
597
598 `extra_css` is a path to a file of other CSS to apply on the page.
599 It will be copied into the HTML directory.
600
601 `title` is a text string (not HTML) to use as the title of the HTML
602 report.
603
604 See `coverage.report()` for other arguments.
605
606 Returns a float, the total percentage covered.
607
608 """
609 self._harvest_data()
610 self.config.from_args(
611 ignore_errors=ignore_errors, omit=omit, include=include,
612 html_dir=directory, extra_css=extra_css, html_title=title,
613 )
614 reporter = HtmlReporter(self, self.config)
615 return reporter.report(morfs)
616
617 def xml_report(self, morfs=None, outfile=None, ignore_errors=None,
618 omit=None, include=None):
619 """Generate an XML report of coverage results.
620
621 The report is compatible with Cobertura reports.
622
623 Each module in `morfs` is included in the report. `outfile` is the
624 path to write the file to, "-" will write to stdout.
625
626 See `coverage.report()` for other arguments.
627
628 Returns a float, the total percentage covered.
629
630 """
631 self._harvest_data()
632 self.config.from_args(
633 ignore_errors=ignore_errors, omit=omit, include=include,
634 xml_output=outfile,
635 )
636 file_to_close = None
637 delete_file = False
638 if self.config.xml_output:
639 if self.config.xml_output == '-':
640 outfile = sys.stdout
641 else:
642 outfile = open(self.config.xml_output, "w")
643 file_to_close = outfile
644 try:
645 try:
646 reporter = XmlReporter(self, self.config)
647 return reporter.report(morfs, outfile=outfile)
648 except CoverageException:
649 delete_file = True
650 raise
651 finally:
652 if file_to_close:
653 file_to_close.close()
654 if delete_file:
655 file_be_gone(self.config.xml_output)
656
657 def sysinfo(self):
658 """Return a list of (key, value) pairs showing internal information."""
659
660 import coverage as covmod
661 import platform, re
662
663 try:
664 implementation = platform.python_implementation()
665 except AttributeError:
666 implementation = "unknown"
667
668 info = [
669 ('version', covmod.__version__),
670 ('coverage', covmod.__file__),
671 ('cover_dir', self.cover_dir),
672 ('pylib_dirs', self.pylib_dirs),
673 ('tracer', self.collector.tracer_name()),
674 ('config_files', self.config.attempted_config_files),
675 ('configs_read', self.config.config_files),
676 ('data_path', self.data.filename),
677 ('python', sys.version.replace('\n', '')),
678 ('platform', platform.platform()),
679 ('implementation', implementation),
680 ('executable', sys.executable),
681 ('cwd', os.getcwd()),
682 ('path', sys.path),
683 ('environment', [
684 ("%s = %s" % (k, v)) for k, v in iitems(os.environ)
685 if re.search(r"^COV|^PY", k)
686 ]),
687 ]
688 return info
689
690
691 def process_startup():
692 """Call this at Python startup to perhaps measure coverage.
693
694 If the environment variable COVERAGE_PROCESS_START is defined, coverage
695 measurement is started. The value of the variable is the config file
696 to use.
697
698 There are two ways to configure your Python installation to invoke this
699 function when Python starts:
700
701 #. Create or append to sitecustomize.py to add these lines::
702
703 import coverage
704 coverage.process_startup()
705
706 #. Create a .pth file in your Python installation containing::
707
708 import coverage; coverage.process_startup()
709
710 """
711 cps = os.environ.get("COVERAGE_PROCESS_START")
712 if cps:
713 cov = coverage(config_file=cps, auto_data=True)
714 cov.start()
715 cov._warn_no_data = False
716 cov._warn_unimported_source = False
717
718
719 # A hack for debugging testing in subprocesses.
720 _TEST_NAME_FILE = "" #"/tmp/covtest.txt"
OLDNEW
« no previous file with comments | « third_party/coverage-3.6/coverage/config.py ('k') | third_party/coverage-3.6/coverage/data.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698