diff options
123 files changed, 4533 insertions, 2856 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bff28ab --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +# This file is for unifying the coding style for different editors and IDEs. +# More information at http://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 80 +trim_trailing_whitespace = true + +[*.py] +max_line_length = 100 + +[*.c] +max_line_length = 100 + +[*.h] +max_line_length = 100 + +[*.yml] +indent_size = 2 + +[*.rst] +max_line_length = 79 + +[Makefile] +indent_style = tab +indent_size = 8 @@ -27,6 +27,7 @@ MANIFEST setuptools-*.egg .tox* .noseids +.cache # Stuff in the test directory. zipmods.zip diff --git a/.travis.yml b/.travis.yml index 1b86b34..986deb6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,21 +8,35 @@ sudo: false python: - 2.7 +addons: + apt: + sources: + - deadsnakes + packages: + - python3.5 + - python3.5-dev + env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - TOXENV=py34 + - TOXENV=py35 - TOXENV=pypy + - TOXENV=py26 COVERAGE_COVERAGE=yes - TOXENV=py27 COVERAGE_COVERAGE=yes + - TOXENV=py33 COVERAGE_COVERAGE=yes + - TOXENV=py34 COVERAGE_COVERAGE=yes + - TOXENV=py35 COVERAGE_COVERAGE=yes + - TOXENV=pypy COVERAGE_COVERAGE=yes sudo: false install: - - pip install -r requirements/tox.pip + - pip install -r requirements/ci.pip script: - tox - - if [ $COVERAGE_COVERAGE == 'yes' ]; then python igor.py combine_html; fi - - if [ $COVERAGE_COVERAGE == 'yes' ]; then pip install codecov; fi - - if [ $COVERAGE_COVERAGE == 'yes' ]; then codecov; fi + - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then python igor.py combine_html; fi + - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then pip install codecov; fi + - if [[ $COVERAGE_COVERAGE == 'yes' ]]; then codecov -X gcov --file coverage.xml; fi diff --git a/CHANGES.rst b/CHANGES.rst index 6ef3ac6..2f57279 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,9 +5,158 @@ Change history for Coverage.py ============================== +.. _changes_44: + Unreleased ---------- +- Code that uses ``sys.settrace(sys.gettrace())`` in a file that wasn't being + coverage-measured would prevent correct coverage measurement in following + code. An example of this was running doctests programmatically, as described + in `issue 575`_. This is now fixed. + +- Errors printed by the ``coverage`` command now go to stderr instead of + stdout. + +- Running ``coverage xml`` in a directory named with non-ASCII characters would + fail under Python 2, as reported in `issue 573`_. This is now fixed. + +.. _issue 573: https://bitbucket.org/ned/coveragepy/issues/573/cant-generate-xml-report-if-some-source +.. _issue 575: https://bitbucket.org/ned/coveragepy/issues/575/running-doctest-prevents-complete-coverage + + +Version 4.4b1 --- 2017-04-04 +---------------------------- + +- Some warnings can now be individually disabled. Warnings that can be + disabled have a short name appended. The ``[run] disable_warnings`` setting + takes a list of these warning names to disable. Closes both `issue 96`_ and + `issue 355`_. + +- The XML report now includes attributes from version 4 of the Cobertura XML + format, fixing `issue 570`_. + +- In previous versions, calling a method that used collected data would prevent + further collection. For example, `save()`, `report()`, `html_report()`, and + others would all stop collection. An explicit `start()` was needed to get it + going again. This is no longer true. Now you can use the collected data and + also continue measurement. Both `issue 79`_ and `issue 448`_ described this + problem, and have been fixed. + +- Plugins can now find unexecuted files if they choose, by implementing the + `find_executable_files` method. Thanks, Emil Madsen. + +- Minimal IronPython support. You should be able to run IronPython programs + under ``coverage run``, though you will still have to do the reporting phase + with CPython. + +- Coverage.py has long had a special hack to support CPython's need to measure + the coverage of the standard library tests. This code was not installed by + kitted versions of coverage.py. Now it is. + +.. _issue 79: https://bitbucket.org/ned/coveragepy/issues/79/save-prevents-harvesting-on-stop +.. _issue 96: https://bitbucket.org/ned/coveragepy/issues/96/unhelpful-warnings-produced-when-using +.. _issue 355: https://bitbucket.org/ned/coveragepy/issues/355/warnings-should-be-suppressable +.. _issue 448: https://bitbucket.org/ned/coveragepy/issues/448/save-and-html_report-prevent-further +.. _issue 570: https://bitbucket.org/ned/coveragepy/issues/570/cobertura-coverage-04dtd-support + + +.. _changes_434: + +Version 4.3.4 --- 2017-01-17 +---------------------------- + +- Fixing 2.6 in version 4.3.3 broke other things, because the too-tricky + exception wasn't properly derived from Exception, described in `issue 556`_. + A newb mistake; it hasn't been a good few days. + +.. _issue 556: https://bitbucket.org/ned/coveragepy/issues/556/43-fails-if-there-are-html-files-in-the + + +.. _changes_433: + +Version 4.3.3 --- 2017-01-17 +---------------------------- + +- Python 2.6 support was broken due to a testing exception imported for the + benefit of the coverage.py test suite. Properly conditionalizing it fixed + `issue 554`_ so that Python 2.6 works again. + +.. _issue 554: https://bitbucket.org/ned/coveragepy/issues/554/traceback-on-python-26-starting-with-432 + + +.. _changes_432: + +Version 4.3.2 --- 2017-01-16 +---------------------------- + +- Using the ``--skip-covered`` option on an HTML report with 100% coverage + would cause a "No data to report" error, as reported in `issue 549`_. This is + now fixed; thanks, Loïc Dachary. + +- If-statements can be optimized away during compilation, for example, `if 0:` + or `if __debug__:`. Coverage.py had problems properly understanding these + statements which existed in the source, but not in the compiled bytecode. + This problem, reported in `issue 522`_, is now fixed. + +- If you specified ``--source`` as a directory, then coverage.py would look for + importable Python files in that directory, and could identify ones that had + never been executed at all. But if you specified it as a package name, that + detection wasn't performed. Now it is, closing `issue 426`_. Thanks to Loïc + Dachary for the fix. + +- If you started and stopped coverage measurement thousands of times in your + process, you could crash Python with a "Fatal Python error: deallocating + None" error. This is now fixed. Thanks to Alex Groce for the bug report. + +- On PyPy, measuring coverage in subprocesses could produce a warning: "Trace + function changed, measurement is likely wrong: None". This was spurious, and + has been suppressed. + +- Previously, coverage.py couldn't start on Jython, due to that implementation + missing the multiprocessing module (`issue 551`_). This problem has now been + fixed. Also, `issue 322`_ about not being able to invoke coverage + conveniently, seems much better: ``jython -m coverage run myprog.py`` works + properly. + +- Let's say you ran the HTML report over and over again in the same output + directory, with ``--skip-covered``. And imagine due to your heroic + test-writing efforts, a file just acheived the goal of 100% coverage. With + coverage.py 4.3, the old HTML file with the less-than-100% coverage would be + left behind. This file is now properly deleted. + +.. _issue 322: https://bitbucket.org/ned/coveragepy/issues/322/cannot-use-coverage-with-jython +.. _issue 426: https://bitbucket.org/ned/coveragepy/issues/426/difference-between-coverage-results-with +.. _issue 522: https://bitbucket.org/ned/coveragepy/issues/522/incorrect-branch-reporting-with-__debug__ +.. _issue 549: https://bitbucket.org/ned/coveragepy/issues/549/skip-covered-with-100-coverage-throws-a-no +.. _issue 551: https://bitbucket.org/ned/coveragepy/issues/551/coveragepy-cannot-be-imported-in-jython27 + + +.. _changes_431: + +Version 4.3.1 --- 2016-12-28 +---------------------------- + +- Some environments couldn't install 4.3, as described in `issue 540`_. This is + now fixed. + +- The check for conflicting ``--source`` and ``--include`` was too simple in a + few different ways, breaking a few perfectly reasonable use cases, described + in `issue 541`_. The check has been reverted while we re-think the fix for + `issue 265`_. + +.. _issue 540: https://bitbucket.org/ned/coveragepy/issues/540/cant-install-coverage-v43-into-under +.. _issue 541: https://bitbucket.org/ned/coveragepy/issues/541/coverage-43-breaks-nosetest-with-coverage + + +.. _changes_43: + +Version 4.3 --- 2016-12-27 +-------------------------- + +Special thanks to **Loïc Dachary**, who took an extraordinary interest in +coverage.py and contributed a number of improvements in this release. + - Subprocesses that are measured with `automatic subprocess measurement`_ used to read in any pre-existing data file. This meant data would be incorrectly carried forward from run to run. Now those files are not read, so each @@ -15,33 +164,122 @@ Unreleased - The ``coverage combine`` command will now fail if there are no data files to combine. The combine changes in 4.2 meant that multiple combines could lose - data, leaving you with an empty .coverage data file. Fixes issues + data, leaving you with an empty .coverage data file. Fixes `issue 525`_, `issue 412`_, `issue 516`_, and probably `issue 511`_. +- Coverage.py wouldn't execute `sys.excepthook`_ when an exception happened in + your program. Now it does, thanks to Andrew Hoos. Closes `issue 535`_. + +- Branch coverage fixes: + + - Branch coverage could misunderstand a finally clause on a try block that + never continued on to the following statement, as described in `issue + 493`_. This is now fixed. Thanks to Joe Doherty for the report and Loïc + Dachary for the fix. + + - A while loop with a constant condition (while True) and a continue + statement would be mis-analyzed, as described in `issue 496`_. This is now + fixed, thanks to a bug report by Eli Skeggs and a fix by Loïc Dachary. + + - While loops with constant conditions that were never executed could result + in a non-zero coverage report. Artem Dayneko reported this in `issue + 502`_, and Loïc Dachary provided the fix. + +- The HTML report now supports a ``--skip-covered`` option like the other + reporting commands. Thanks, Loïc Dachary for the implementation, closing + `issue 433`_. + +- Options can now be read from a tox.ini file, if any. Like setup.cfg, sections + are prefixed with "coverage:", so ``[run]`` options will be read from the + ``[coverage:run]`` section of tox.ini. Implements part of `issue 519`_. + Thanks, Stephen Finucane. + +- Specifying both ``--source`` and ``--include`` no longer silently ignores the + include setting, instead it fails with a message. Thanks, Nathan Land and + Loïc Dachary. Closes `issue 265`_. + +- The ``Coverage.combine`` method has a new parameter, ``strict=False``, to + support failing if there are no data files to combine. + - When forking subprocesses, the coverage data files would have the same random number appended to the file name. This didn't cause problems, because the file names had the process id also, making collisions (nearly) impossible. But it was disconcerting. This is now fixed. +- The text report now properly sizes headers when skipping some files, fixing + `issue 524`_. Thanks, Anthony Sottile and Loïc Dachary. + +- Coverage.py can now search .pex files for source, just as it can .zip and + .egg. Thanks, Peter Ebden. + - Data files are now about 15% smaller. +- Improvements in the ``[run] debug`` setting: + + - The "dataio" debug setting now also logs when data files are deleted during + combining or erasing. + + - A new debug option, "multiproc", for logging the behavior of + ``concurrency=multiprocessing``. + + - If you used the debug options "config" and "callers" together, you'd get a + call stack printed for every line in the multi-line config output. This is + now fixed. + +- Fixed an unusual bug involving multiple coding declarations affecting code + containing code in multi-line strings: `issue 529`_. + +- Coverage.py will no longer be misled into thinking that a plain file is a + package when interpreting ``--source`` options. Thanks, Cosimo Lupo. + +- If you try to run a non-Python file with coverage.py, you will now get a more + useful error message. `Issue 514`_. + +- The default pragma regex changed slightly, but this will only matter to you + if you are deranged and use mixed-case pragmas. + +- Deal properly with non-ASCII file names in an ASCII-only world, `issue 533`_. + +- Programs that set Unicode configuration values could cause UnicodeErrors when + generating HTML reports. Pytest-cov is one example. This is now fixed. + +- Prevented deprecation warnings from configparser that happened in some + circumstances, closing `issue 530`_. + - Corrected the name of the jquery.ba-throttle-debounce.js library. Thanks, Ben Finney. Closes `issue 505`_. -- Support PyPy3 5.2 alpha 1. +- Testing against PyPy 5.6 and PyPy3 5.5. -- If you used the debug options "config" and "callers" together, you'd get a - call stack printed for every line in the multi-line config output. This is - now fixed. +- Switched to pytest from nose for running the coverage.py tests. + +- Renamed AUTHORS.txt to CONTRIBUTORS.txt, since there are other ways to + contribute than by writing code. Also put the count of contributors into the + author string in setup.py, though this might be too cute. +.. _sys.excepthook: https://docs.python.org/3/library/sys.html#sys.excepthook +.. _issue 265: https://bitbucket.org/ned/coveragepy/issues/265/when-using-source-include-is-silently .. _issue 412: https://bitbucket.org/ned/coveragepy/issues/412/coverage-combine-should-error-if-no +.. _issue 433: https://bitbucket.org/ned/coveragepy/issues/433/coverage-html-does-not-suport-skip-covered +.. _issue 493: https://bitbucket.org/ned/coveragepy/issues/493/confusing-branching-failure +.. _issue 496: https://bitbucket.org/ned/coveragepy/issues/496/incorrect-coverage-with-branching-and +.. _issue 502: https://bitbucket.org/ned/coveragepy/issues/502/incorrect-coverage-report-with-cover .. _issue 505: https://bitbucket.org/ned/coveragepy/issues/505/use-canonical-filename-for-debounce +.. _issue 514: https://bitbucket.org/ned/coveragepy/issues/514/path-to-problem-file-not-reported-when .. _issue 510: https://bitbucket.org/ned/coveragepy/issues/510/erase-still-needed-in-42 .. _issue 511: https://bitbucket.org/ned/coveragepy/issues/511/version-42-coverage-combine-empties .. _issue 516: https://bitbucket.org/ned/coveragepy/issues/516/running-coverage-combine-twice-deletes-all +.. _issue 519: https://bitbucket.org/ned/coveragepy/issues/519/coverage-run-sections-in-toxini-or-as +.. _issue 524: https://bitbucket.org/ned/coveragepy/issues/524/coverage-report-with-skip-covered-column .. _issue 525: https://bitbucket.org/ned/coveragepy/issues/525/coverage-combine-when-not-in-parallel-mode +.. _issue 529: https://bitbucket.org/ned/coveragepy/issues/529/encoding-marker-may-only-appear-on-the +.. _issue 530: https://bitbucket.org/ned/coveragepy/issues/530/deprecationwarning-you-passed-a-bytestring +.. _issue 533: https://bitbucket.org/ned/coveragepy/issues/533/exception-on-unencodable-file-name +.. _issue 535: https://bitbucket.org/ned/coveragepy/issues/535/sysexcepthook-is-not-called +.. _changes_42: + Version 4.2 --- 2016-07-26 -------------------------- @@ -119,6 +357,8 @@ Work from the PyCon 2016 Sprints! .. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins +.. _changes_41: + Version 4.1 --- 2016-05-21 -------------------------- @@ -273,6 +513,8 @@ Version 4.1b1 --- 2016-01-10 .. _issue 461: https://bitbucket.org/ned/coveragepy/issues/461/multiline-asserts-need-too-many-pragma +.. _changes_403: + Version 4.0.3 --- 2015-11-24 ---------------------------- @@ -298,6 +540,8 @@ Version 4.0.3 --- 2015-11-24 .. _issue 445: https://bitbucket.org/ned/coveragepy/issues/445/django-app-cannot-connect-to-cassandra +.. _changes_402: + Version 4.0.2 --- 2015-11-04 ---------------------------- @@ -317,6 +561,8 @@ Version 4.0.2 --- 2015-11-04 .. _issue 436: https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from +.. _changes_401: + Version 4.0.1 --- 2015-10-13 ---------------------------- @@ -363,6 +609,8 @@ Version 4.0.1 --- 2015-10-13 .. _issue 423: https://bitbucket.org/ned/coveragepy/issues/423/skip_covered-changes-reported-total +.. _changes_40: + Version 4.0 --- 2015-09-20 -------------------------- @@ -389,7 +637,6 @@ Version 4.0b3 --- 2015-09-07 .. _issue 404: https://bitbucket.org/ned/coveragepy/issues/404/shiningpanda-jenkins-plugin-cant-find-html - Version 4.0b2 --- 2015-08-22 ---------------------------- @@ -764,6 +1011,8 @@ Version 4.0a1 --- 2014-09-27 .. _issue 331: https://bitbucket.org/ned/coveragepy/issue/331/failure-of-encoding-detection-on-python2 +.. _changes_371: + Version 3.7.1 --- 2013-12-13 ---------------------------- @@ -773,6 +1022,8 @@ Version 3.7.1 --- 2013-12-13 so that it will actually find OS-installed static files. +.. _changes_37: + Version 3.7 --- 2013-10-06 -------------------------- @@ -830,6 +1081,8 @@ Version 3.7 --- 2013-10-06 .. _issue 267: https://bitbucket.org/ned/coveragepy/issue/267/relative-path-aliases-dont-work +.. _changes_36: + Version 3.6 --- 2013-01-05 -------------------------- @@ -982,6 +1235,8 @@ Version 3.6b1 --- 2012-11-28 .. _issue 214: https://bitbucket.org/ned/coveragepy/issue/214/coveragepy-measures-itself-on-precise +.. _changes_353: + Version 3.5.3 --- 2012-09-29 ---------------------------- @@ -1021,6 +1276,8 @@ Version 3.5.3 --- 2012-09-29 .. _tox: http://tox.readthedocs.org/ +.. _changes_352: + Version 3.5.2 --- 2012-05-04 ---------------------------- @@ -1071,6 +1328,8 @@ Version 3.5.2b1 --- 2012-04-29 .. _issue 173: https://bitbucket.org/ned/coveragepy/issue/173/theres-no-way-to-specify-show-missing-in +.. _changes_351: + Version 3.5.1 --- 2011-09-23 ---------------------------- @@ -1118,6 +1377,8 @@ Version 3.5.1b1 --- 2011-08-28 .. _issue 144: http://bitbucket.org/ned/coveragepy/issue/144/failure-generating-html-output-for +.. _changes_35: + Version 3.5 --- 2011-06-29 -------------------------- @@ -1206,6 +1467,8 @@ Version 3.5b1 --- 2011-06-05 .. _issue 125: https://bitbucket.org/ned/coveragepy/issue/125/coverage-removes-decoratortoolss-tracing +.. _changes_34: + Version 3.4 --- 2010-09-19 -------------------------- @@ -1333,6 +1596,8 @@ Version 3.4b1 --- 2010-08-21 .. _issue 82: http://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _changes_331: + Version 3.3.1 --- 2010-03-06 ---------------------------- @@ -1346,6 +1611,8 @@ Version 3.3.1 --- 2010-03-06 .. _issue 50: http://bitbucket.org/ned/coveragepy/issue/50 +.. _changes_33: + Version 3.3 --- 2010-02-24 -------------------------- @@ -1385,6 +1652,8 @@ Version 3.3 --- 2010-02-24 .. _issue 47: http://bitbucket.org/ned/coveragepy/issue/47 +.. _changes_32: + Version 3.2 --- 2009-12-05 -------------------------- @@ -1404,7 +1673,7 @@ Version 3.2b4 --- 2009-12-01 - On Python 3.x, setuptools has been replaced by `Distribute`_. -.. _Distribute: http://packages.python.org/distribute/ +.. _Distribute: https://pypi.python.org/pypi/distribute Version 3.2b3 --- 2009-11-23 @@ -1461,6 +1730,8 @@ Version 3.2b1 --- 2009-11-10 .. _issue 23: http://bitbucket.org/ned/coveragepy/issue/23 +.. _changes_31: + Version 3.1 --- 2009-10-04 -------------------------- @@ -1503,6 +1774,8 @@ Version 3.1b1 --- 2009-09-27 .. _issue 24: http://bitbucket.org/ned/coveragepy/issue/24 +.. _changes_301: + Version 3.0.1 --- 2009-07-07 ---------------------------- @@ -1528,6 +1801,8 @@ Version 3.0.1 --- 2009-07-07 .. _issue 8: http://bitbucket.org/ned/coveragepy/issue/8 +.. _changes_30: + Version 3.0 --- 2009-06-13 -------------------------- diff --git a/AUTHORS.txt b/CONTRIBUTORS.txt index 201cac3..177bf97 100644 --- a/AUTHORS.txt +++ b/CONTRIBUTORS.txt @@ -1,26 +1,34 @@ Coverage.py was originally written by Gareth Rees, and since 2004 has been extended and maintained by Ned Batchelder. -Other contributions have been made by: +Other contributions, including writing code, updating docs, and submitting +useful bug reports, have been made by: Adi Roiban Alex Gaynor +Alex Groce Alexander Todorov +Andrew Hoos Anthony Sottile Arcadiy Ivanov +Aron Griffis +Artem Dayneko Ben Finney Bill Hart Brandon Rhodes Brett Cannon Buck Evan +Calen Pennington Carl Gieringer Catherine Proulx Chris Adams +Chris Jerdonek Chris Rose Christian Heimes Christine Lytwynec Christoph Zwerschke Conrad Ho +Cosimo Lupo Dan Riti Dan Wandschneider Danek Duvall @@ -32,6 +40,7 @@ Devin Jeanpierre Dmitry Shishov Dmitry Trofimov Eduardo Schettino +Emil Madsen Edward Loper Geoff Bache George Paci @@ -43,13 +52,16 @@ Imri Goldberg Ionel Cristian Mărieș JT Olds Jessamyn Smith +Joe Doherty Jon Chappell Joseph Tate Josh Williams Julian Berman Krystian Kichewko +Kyle Altendorf Leonardo Pistone Lex Berezhny +Loïc Dachary Marc Abramowitz Marcus Cobden Mark van der Wal @@ -62,6 +74,7 @@ Nathan Land Noel O'Boyle Pablo Carballo Patrick Mezard +Peter Ebden Peter Portante Rodrigue Cloutier Roger Hu @@ -72,6 +85,7 @@ Scott Belden Sigve Tjora Stan Hu Stefan Behnel +Stephen Finucane Steve Leonard Steve Peak Ted Wexler diff --git a/MANIFEST.in b/MANIFEST.in index 31e2230..462f24f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,7 @@ # MANIFEST.in file for coverage.py -include AUTHORS.txt +include CONTRIBUTORS.txt include CHANGES.rst include LICENSE.txt include MANIFEST.in @@ -7,6 +7,7 @@ default: @echo "* No default action *" clean: + -pip uninstall -y coverage -rm -f *.pyd */*.pyd -rm -f *.so */*.so -PYTHONPATH=. python tests/test_farm.py clean @@ -19,31 +20,41 @@ clean: -rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ -rm -f coverage/*,cover -rm -f MANIFEST - -rm -f .coverage .coverage.* coverage.xml .metacov* .noseids + -rm -f .coverage .coverage.* coverage.xml .metacov* -rm -f tests/zipmods.zip -rm -rf tests/eggsrc/build tests/eggsrc/dist tests/eggsrc/*.egg-info -rm -f setuptools-*.egg distribute-*.egg distribute-*.tar.gz -rm -rf doc/_build doc/_spell + -rm -rf .tox_kits sterile: clean -rm -rf .tox* -LINTABLE = coverage igor.py setup.py tests ci/*.py +LINTABLE = coverage tests igor.py setup.py __main__.py lint: - -pylint $(LINTABLE) - python -m tabnanny $(LINTABLE) - python igor.py check_eol + tox -e lint + +todo: + -grep -R --include=*.py TODO $(LINTABLE) spell: -pylint --disable=all --enable=spelling $(LINTABLE) pep8: - pep8 --filename=*.py --repeat $(LINTABLE) + pycodestyle --filename=*.py --repeat $(LINTABLE) test: tox -e py27,py35 $(ARGS) +TOX_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) + +smoke: + COVERAGE_NO_PYTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS) + +pysmoke: + COVERAGE_NO_CTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS) + metacov: COVERAGE_COVERAGE=yes tox $(ARGS) @@ -53,11 +64,15 @@ metahtml: # Kitting kit: - python setup.py sdist --formats=gztar,zip + python setup.py sdist --formats=gztar wheel: tox -c tox_wheels.ini $(ARGS) +manylinux: + docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/ci/manylinux.sh build + docker run --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/ci/manylinux.sh build + kit_upload: twine upload dist/* @@ -73,9 +88,6 @@ kit_local: download_appveyor: python ci/download_appveyor.py nedbat/coveragepy -pypi: - python setup.py register - build_ext: python setup.py build_ext @@ -98,7 +110,7 @@ docreqs: pip install -r doc/requirements.pip dochtml: - $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html + PYTHONPATH=$(CURDIR) $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html @echo @echo "Build finished. The HTML pages are in doc/_build/html." @@ -1,5 +1,5 @@ Copyright 2001 Gareth Rees. All rights reserved. -Copyright 2004-2016 Ned Batchelder. All rights reserved. +Copyright 2004-2017 Ned Batchelder. All rights reserved. Except where noted otherwise, this software is licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in @@ -9,20 +9,36 @@ Code coverage testing for Python. | |license| |versions| |status| |docs| | |ci-status| |win-ci-status| |codecov| -| |kit| |format| |downloads| +| |kit| |format| |commits-since| +| |saythanks| + +.. downloads badge seems to be broken... |downloads| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard library to determine which lines are executable, and which have been executed. -Coverage.py runs on CPython 2.6, 2.7, and 3.3 through 3.6; PyPy 4.0 and 5.1; -and PyPy3 2.4 and 5.2. +Coverage.py runs on many versions of Python: + +* CPython 2.6, 2.7 and 3.3 through 3.6. +* PyPy2 5.6 and PyPy3 5.5. +* Jython 2.7.1, though not for reporting. +* IronPython 2.7.7, though not for reporting. + +Documentation is on `Read the Docs`_. Code repository and issue tracker are on +`Bitbucket`_, with a mirrored repository on `GitHub`_. + +.. _Read the Docs: http://coverage.readthedocs.io +.. _Bitbucket: http://bitbucket.org/ned/coveragepy +.. _GitHub: https://github.com/nedbat/coveragepy -Documentation is on `Read the Docs <http://coverage.readthedocs.io>`_. -Code repository and issue tracker are on `Bitbucket <http://bitbucket.org/ned/coveragepy>`_, -with a mirrored repository on `GitHub <https://github.com/nedbat/coveragepy>`_. -**New in 4.2:** better support for multiprocessing and combining data. +**New in 4.4:** Suppressable warnings, continuous coverage measurement. + +New in 4.3: HTML ``--skip-covered``, sys.excepthook support, tox.ini +support. + +New in 4.2: better support for multiprocessing and combining data. New in 4.1: much-improved branch coverage. @@ -33,21 +49,24 @@ support, --skip-covered, HTML filtering, and more than 50 issues closed. Getting Started --------------- -See the `quick start <http://coverage.readthedocs.io/#quick-start>`_ -section of the docs. +See the `Quick Start`_ section of the docs. + +.. _Quick Start: http://coverage.readthedocs.io/#quick-start License ------- -Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0. -For details, see https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt. +Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. + +.. _Apache 2.0 License: http://www.apache.org/licenses/LICENSE-2.0 +.. _NOTICE.txt: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt .. |ci-status| image:: https://travis-ci.org/nedbat/coveragepy.svg?branch=master :target: https://travis-ci.org/nedbat/coveragepy :alt: Build status -.. |win-ci-status| image:: https://ci.appveyor.com/api/projects/status/bitbucket/ned/coveragepy?svg=true +.. |win-ci-status| image:: https://ci.appveyor.com/api/projects/status/kmeqpdje7h9r6vsf/branch/master?svg=true :target: https://ci.appveyor.com/project/nedbat/coveragepy :alt: Windows build status .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat @@ -74,6 +93,12 @@ For details, see https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt. .. |license| image:: https://img.shields.io/pypi/l/coverage.svg :target: https://pypi.python.org/pypi/coverage :alt: License -.. |codecov| image:: http://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master +.. |codecov| image:: http://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 :target: http://codecov.io/github/nedbat/coveragepy?branch=master :alt: Coverage! +.. |commits-since| image:: https://img.shields.io/github/commits-since/nedbat/coveragepy/coverage-4.3.4.svg + :target: https://github.com/nedbat/coveragepy/compare/coverage-4.3.4...master + :alt: See latest work +.. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg + :target: https://saythanks.io/to/nedbat + :alt: Say thanks :) diff --git a/__main__.py b/__main__.py index a44b820..c998e1d 100644 --- a/__main__.py +++ b/__main__.py @@ -3,7 +3,8 @@ """Be able to execute coverage.py by pointing Python at a working tree.""" -import runpy, os +import runpy +import os PKG = 'coverage' diff --git a/appveyor.yml b/appveyor.yml index 914e297..aa95798 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,6 +9,10 @@ environment: CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\run_with_env.cmd" + # Parallel pytest gets tangled up with tests that try to create and destroy + # .pth files in the shared virtualenv. Disable parallel tests. + PYTEST_ADDOPTS: "-n 0" + matrix: - JOB: "2.6 32-bit" TOXENV: "py26" @@ -58,6 +62,18 @@ environment: PYTHON_VERSION: "3.5.0" PYTHON_ARCH: "64" + - JOB: "3.6 32-bit" + TOXENV: "py36" + PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.0" + PYTHON_ARCH: "32" + + - JOB: "3.6 64-bit" + TOXENV: "py36" + PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.0" + PYTHON_ARCH: "64" + # Meta coverage - JOB: "Meta 2.7" TOXENV: "py27" @@ -66,10 +82,10 @@ environment: PYTHON_ARCH: "32" COVERAGE_COVERAGE: "yes" - - JOB: "Meta 3.4" - TOXENV: "py34" - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4" + - JOB: "Meta 3.5" + TOXENV: "py35" + PYTHON: "C:\\Python35" + PYTHON_VERSION: "3.5" PYTHON_ARCH: "32" COVERAGE_COVERAGE: "yes" @@ -97,7 +113,7 @@ install: - "pip install --disable-pip-version-check --user --upgrade virtualenv" # Install requirements. - - "%CMD_IN_ENV% pip install -r requirements/tox.pip -r requirements/wheel.pip" + - "%CMD_IN_ENV% pip install -r requirements/ci.pip" # Make a python3.4.bat file in the current directory so that tox will find it # and python3.4 will mean what we want it to. @@ -115,6 +131,9 @@ test_script: after_test: - if "%COVERAGE_COVERAGE%" == "yes" 7z a metacov-win-%TOXENV%.zip %APPVEYOR_BUILD_FOLDER%\.metacov* + - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% %PYTHON%\python igor.py combine_html + - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% pip install codecov + - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% codecov -X gcov --file coverage.xml artifacts: - path: "metacov-*.zip" diff --git a/ci/download_appveyor.py b/ci/download_appveyor.py index f640b41..daf6f06 100644 --- a/ci/download_appveyor.py +++ b/ci/download_appveyor.py @@ -38,10 +38,10 @@ def download_latest_artifacts(account_project): """Download all the artifacts from the latest build.""" build = get_project_build(account_project) jobs = build['build']['jobs'] - print "Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs)) + print("Build {0[build][version]}, {1} jobs: {0[build][message]}".format(build, len(jobs))) for job in jobs: name = job['name'].partition(':')[2].split(',')[0].strip() - print " {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job) + print(" {0}: {1[status]}, {1[artifactsCount]} artifacts".format(name, job)) url = make_url("/buildjobs/{jobid}/artifacts", jobid=job['jobId']) response = requests.get(url, headers=make_auth_headers()) @@ -50,7 +50,7 @@ def download_latest_artifacts(account_project): for artifact in artifacts: is_zip = artifact['type'] == "Zip" filename = artifact['fileName'] - print " {0}, {1} bytes".format(filename, artifact['size']) + print(" {0}, {1} bytes".format(filename, artifact['size'])) url = make_url( "/buildjobs/{jobid}/artifacts/{filename}", @@ -86,7 +86,7 @@ def unpack_zipfile(filename): with open(filename, 'rb') as fzip: z = zipfile.ZipFile(fzip) for name in z.namelist(): - print " extracting {0}".format(name) + print(" extracting {0}".format(name)) ensure_dirs(name) z.extract(name) diff --git a/ci/manylinux.sh b/ci/manylinux.sh new file mode 100755 index 0000000..98dc874 --- /dev/null +++ b/ci/manylinux.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# From: https://github.com/pypa/python-manylinux-demo/blob/master/travis/build-wheels.sh +# which is in the public domain. +# +# This is run inside a CentOS 5 virtual machine to build manylinux wheels: +# +# $ docker run -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/ci/build_manylinux.sh +# + +set -e -x + +action=$1 + +if [[ $action == "build" ]]; then + # Compile wheels + cd /io + for PYBIN in /opt/python/*/bin; do + "$PYBIN/pip" install -r requirements/wheel.pip + "$PYBIN/python" setup.py bdist_wheel -d ~/wheelhouse/ + done + cd ~ + + # Bundle external shared libraries into the wheels + for whl in wheelhouse/*.whl; do + auditwheel repair "$whl" -w /io/dist/ + done + +elif [[ $action == "test" ]]; then + # Install packages and test + TOXBIN=/opt/python/cp27-cp27m/bin + "$TOXBIN/pip" install -r /io/requirements/ci.pip + + for PYBIN in /opt/python/*/bin/; do + PYNAME=$("$PYBIN/python" -c "import sys; print('python{0[0]}.{0[1]}'.format(sys.version_info))") + TOXENV=$("$PYBIN/python" -c "import sys; print('py{0[0]}{0[1]}'.format(sys.version_info))") + ln -s "$PYBIN/$PYNAME" /usr/local/bin/$PYNAME + "$TOXBIN/tox" -e $TOXENV + rm -f /usr/local/bin/$PYNAME + #"${PYBIN}/pip" install python-manylinux-demo --no-index -f /io/dist + #(cd "$HOME"; "${PYBIN}/nosetests" pymanylinuxdemo) + done + +else + echo "Need an action to perform!" +fi diff --git a/coverage/backward.py b/coverage/backward.py index 700c3eb..62ca495 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -3,10 +3,8 @@ """Add things to old Pythons so I can pretend they are newer.""" -# This file does lots of tricky stuff, so disable a bunch of pylint warnings. -# pylint: disable=redefined-builtin +# This file does tricky stuff, so disable a pylint warning. # pylint: disable=unused-import -# pxlint: disable=no-name-in-module import sys @@ -19,11 +17,14 @@ try: except ImportError: from io import StringIO -# In py3, ConfigParser was renamed to the more-standard configparser +# In py3, ConfigParser was renamed to the more-standard configparser. +# But there's a py3 backport that installs "configparser" in py2, and I don't +# want it because it has annoying deprecation warnings. So try the real py2 +# import first. try: - import configparser -except ImportError: import ConfigParser as configparser +except ImportError: + import configparser # What's a string called? try: @@ -45,9 +46,9 @@ except ImportError: # range or xrange? try: - range = xrange + range = xrange # pylint: disable=redefined-builtin except NameError: - range = range # pylint: disable=redefined-variable-type + range = range # shlex.quote is new, but there's an undocumented implementation in "pipes", # who knew!? @@ -143,6 +144,12 @@ except AttributeError: PYC_MAGIC_NUMBER = imp.get_magic() +def invalidate_import_caches(): + """Invalidate any import caches that may or may not exist.""" + if importlib and hasattr(importlib, "invalidate_caches"): + importlib.invalidate_caches() + + def import_local_file(modname, modfile=None): """Import a local file as a module. diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 8942024..63e4eb1 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -3,6 +3,8 @@ """Command-line support for coverage.py.""" +from __future__ import print_function + import glob import optparse import os.path @@ -12,9 +14,10 @@ import traceback from coverage import env from coverage.collector import CTracer -from coverage.execfile import run_python_file, run_python_module -from coverage.misc import CoverageException, ExceptionDuringRun, NoSource from coverage.debug import info_formatter, info_header +from coverage.execfile import run_python_file, run_python_module +from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource +from coverage.results import should_fail_under class Opts(object): @@ -248,7 +251,7 @@ class CmdOptionParser(CoverageOptionParser): program_name = super(CmdOptionParser, self).get_prog_name() # Include the sub-command for this parser as part of the command. - return "%(command)s %(subcommand)s" % {'command': program_name, 'subcommand': self.cmd} + return "{command} {subcommand}".format(command=program_name, subcommand=self.cmd) GLOBAL_ARGS = [ @@ -320,6 +323,7 @@ CMDS = { Opts.include, Opts.omit, Opts.title, + Opts.skip_covered, ] + GLOBAL_ARGS, usage="[options] [modules]", description=( @@ -458,7 +462,7 @@ class CoverageScript(object): debug = unshell_list(options.debug) # Do something. - self.coverage = self.covpkg.coverage( + self.coverage = self.covpkg.Coverage( data_suffix=options.parallel_mode, cover_pylib=options.pylib, timid=options.timid, @@ -510,7 +514,7 @@ class CoverageScript(object): elif options.action == "html": total = self.coverage.html_report( directory=options.directory, title=options.title, - **report_args) + skip_covered=options.skip_covered, **report_args) elif options.action == "xml": outfile = options.outfile total = self.coverage.xml_report(outfile=outfile, **report_args) @@ -521,18 +525,9 @@ class CoverageScript(object): if options.fail_under is not None: self.coverage.set_option("report:fail_under", options.fail_under) - if self.coverage.get_option("report:fail_under"): - # Total needs to be rounded, but don't want to report 100 - # unless it is really 100. - if 99 < total < 100: - total = 99 - else: - total = round(total) - - if total >= self.coverage.get_option("report:fail_under"): - return OK - else: - return FAIL_UNDER + fail_under = self.coverage.get_option("report:fail_under") + if should_fail_under(total, fail_under): + return FAIL_UNDER return OK @@ -540,8 +535,8 @@ class CoverageScript(object): """Display an error message, or the named topic.""" assert error or topic or parser if error: - print(error) - print("Use '%s help' for help." % (self.program_name,)) + print(error, file=sys.stderr) + print("Use '%s help' for help." % (self.program_name,), file=sys.stderr) elif parser: print(parser.format_help().strip()) else: @@ -757,9 +752,9 @@ def main(argv=None): # An exception was caught while running the product code. The # sys.exc_info() return tuple is packed into an ExceptionDuringRun # exception. - traceback.print_exception(*err.args) + traceback.print_exception(*err.args) # pylint: disable=no-value-for-parameter status = ERR - except CoverageException as err: + except BaseCoverageException as err: # A controlled error inside coverage.py: print the message to the user. print(err) status = ERR diff --git a/coverage/collector.py b/coverage/collector.py index 3e28b3b..cfdcf40 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -162,6 +162,13 @@ class Collector(object): """Return the class name of the tracer we're using.""" return self._trace_class.__name__ + def _clear_data(self): + """Clear out existing data, but stay ready for more collection.""" + self.data.clear() + + for tracer in self.tracers: + tracer.reset_activity() + def reset(self): """Clear collected data, and prepare to collect more.""" # A dictionary mapping file names to dicts with line number keys (if not @@ -208,6 +215,8 @@ class Collector(object): # Our active Tracers. self.tracers = [] + self._clear_data() + def _start_tracer(self): """Start a new Tracer object, and store it in self.tracers.""" tracer = self._trace_class() @@ -267,6 +276,8 @@ class Collector(object): if self._collectors: self._collectors[-1].pause() + self.tracers = [] + # Check to see whether we had a fullcoverage tracer installed. If so, # get the stack frames it stashed away for us. traces0 = [] @@ -296,7 +307,7 @@ class Collector(object): except TypeError: raise Exception("fullcoverage must be run with the C trace function.") - # Install our installation tracer in threading, to jump start other + # Install our installation tracer in threading, to jump-start other # threads. if self.threading: self.threading.settrace(self._installation_trace) @@ -309,7 +320,6 @@ class Collector(object): ) self.pause() - self.tracers = [] # Remove this Collector from the stack, and resume the one underneath # (if any). @@ -338,6 +348,14 @@ class Collector(object): else: self._start_tracer() + def _activity(self): + """Has any activity been traced? + + Returns a boolean, True if any trace function was invoked. + + """ + return any(tracer.activity() for tracer in self.tracers) + def switch_context(self, new_context): """Who-Tests-What hack: switch to a new who-context.""" # Make a new data dict, or find the existing one, and switch all the @@ -349,9 +367,11 @@ class Collector(object): def save_data(self, covdata): """Save the collected data to a `CoverageData`. - Also resets the collector. - + Returns True if there was data to save, False if not. """ + if not self._activity(): + return False + def abs_file_dict(d): """Return a dict like d, but with keys modified by `abs_file`.""" return dict((abs_file(k), v) for k, v in iitems(d)) @@ -369,4 +389,5 @@ class Collector(object): with open(out_file, "w") as wtw_out: pprint.pprint(self.contexts, wtw_out) - self.reset() + self._clear_data() + return True diff --git a/coverage/config.py b/coverage/config.py index c7d6555..3fa6449 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -21,12 +21,12 @@ class HandyConfigParser(configparser.RawConfigParser): configparser.RawConfigParser.__init__(self) self.section_prefix = section_prefix - def read(self, filename): # pylint: disable=arguments-differ + def read(self, filenames): """Read a file name as UTF-8 configuration data.""" kwargs = {} if sys.version_info >= (3, 2): kwargs['encoding'] = "utf-8" - return configparser.RawConfigParser.read(self, filename, **kwargs) + return configparser.RawConfigParser.read(self, filenames, **kwargs) def has_option(self, section, option): section = self.section_prefix + section @@ -47,7 +47,7 @@ class HandyConfigParser(configparser.RawConfigParser): d[opt] = self.get(section, opt) return d - def get(self, section, *args, **kwargs): + def get(self, section, *args, **kwargs): # pylint: disable=arguments-differ """Get a value, replacing environment variables also. The arguments are the same as `RawConfigParser.get`, but in the found @@ -122,12 +122,12 @@ class HandyConfigParser(configparser.RawConfigParser): # The default line exclusion regexes. DEFAULT_EXCLUDE = [ - r'(?i)#\s*pragma[:\s]?\s*no\s*cover', + r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(cover|COVER)', ] # The default partial branch regexes, to be modified by the user. DEFAULT_PARTIAL = [ - r'(?i)#\s*pragma[:\s]?\s*no\s*branch', + r'#\s*(pragma|PRAGMA)[:\s]?\s*(no|NO)\s*(branch|BRANCH)', ] # The default partial branch regexes, based on Python semantics. @@ -158,6 +158,7 @@ class CoverageConfig(object): self.cover_pylib = False self.data_file = ".coverage" self.debug = [] + self.disable_warnings = [] self.note = None self.parallel = False self.plugins = [] @@ -191,7 +192,7 @@ class CoverageConfig(object): # Options for plugins self.plugin_options = {} - MUST_BE_LIST = ["omit", "include", "debug", "plugins", "concurrency"] + MUST_BE_LIST = ["concurrency", "debug", "disable_warnings", "include", "omit", "plugins"] def from_args(self, **kwargs): """Read config values from `kwargs`.""" @@ -207,7 +208,8 @@ class CoverageConfig(object): `filename` is a file name to read. - Returns True or False, whether the file could be read. + Returns True or False, whether the file could be read, and it had some + coverage.py settings in it. """ self.attempted_config_files.append(filename) @@ -222,9 +224,12 @@ class CoverageConfig(object): self.config_files.extend(files_read) + any_set = False try: for option_spec in self.CONFIG_FILE_OPTIONS: - self._set_attr_from_config_option(cp, *option_spec) + was_set = self._set_attr_from_config_option(cp, *option_spec) + if was_set: + any_set = True except ValueError as err: raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) @@ -249,13 +254,20 @@ class CoverageConfig(object): if cp.has_section('paths'): for option in cp.options('paths'): self.paths[option] = cp.getlist('paths', option) + any_set = True # plugins can have options for plugin in self.plugins: if cp.has_section(plugin): self.plugin_options[plugin] = cp.get_section(plugin) + any_set = True - return True + # Was this file used as a config file? If no prefix, then it was used. + # If a prefix, then it was only used if we found some settings in it. + if section_prefix: + return any_set + else: + return True CONFIG_FILE_OPTIONS = [ # These are *args for _set_attr_from_config_option: @@ -272,6 +284,7 @@ class CoverageConfig(object): ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), + ('disable_warnings', 'run:disable_warnings', 'list'), ('include', 'run:include', 'list'), ('note', 'run:note'), ('omit', 'run:omit', 'list'), @@ -304,11 +317,17 @@ class CoverageConfig(object): ] def _set_attr_from_config_option(self, cp, attr, where, type_=''): - """Set an attribute on self if it exists in the ConfigParser.""" + """Set an attribute on self if it exists in the ConfigParser. + + Returns True if the attribute was set. + + """ section, option = where.split(":") if cp.has_option(section, option): method = getattr(cp, 'get' + type_) setattr(self, attr, method(section, option)) + return True + return False def get_plugin_options(self, plugin): """Get a dictionary of options for the plugin named `plugin`.""" @@ -351,7 +370,6 @@ class CoverageConfig(object): Returns the value of the option. """ - # Check all the hard-coded options. for option_spec in self.CONFIG_FILE_OPTIONS: attr, where = option_spec[:2] @@ -365,3 +383,61 @@ class CoverageConfig(object): # If we get here, we didn't find the option. raise CoverageException("No such option: %r" % option_name) + + +def read_coverage_config(config_file, **kwargs): + """Read the coverage.py configuration. + + Arguments: + config_file: a boolean or string, see the `Coverage` class for the + tricky details. + all others: keyword arguments from the `Coverage` class, used for + setting values in the configuration. + + Returns: + config_file, config: + config_file is the value to use for config_file in other + invocations of coverage. + + config is a CoverageConfig object read from the appropriate + configuration file. + + """ + # Build the configuration from a number of sources: + # 1) defaults: + config = CoverageConfig() + + # 2) from a file: + if config_file: + # Some API users were specifying ".coveragerc" to mean the same as + # True, so make it so. + if config_file == ".coveragerc": + config_file = True + specified_file = (config_file is not True) + if not specified_file: + config_file = ".coveragerc" + + for fname, prefix in [(config_file, ""), + ("setup.cfg", "coverage:"), + ("tox.ini", "coverage:")]: + config_read = config.from_file(fname, section_prefix=prefix) + is_config_file = fname == config_file + + if not config_read and is_config_file and specified_file: + raise CoverageException("Couldn't read '%s' as a config file" % fname) + + if config_read: + break + + # 3) from environment variables: + env_data_file = os.environ.get('COVERAGE_FILE') + if env_data_file: + config.data_file = env_data_file + debugs = os.environ.get('COVERAGE_DEBUG') + if debugs: + config.debug.extend(d.strip() for d in debugs.split(",")) + + # 4) from constructor arguments: + config.from_args(**kwargs) + + return config_file, config diff --git a/coverage/control.py b/coverage/control.py index 351992f..fb03361 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -3,39 +3,47 @@ """Core control stuff for coverage.py.""" + import atexit import inspect +import itertools import os import platform import re import sys import traceback -from coverage import env, files +from coverage import env from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems from coverage.collector import Collector -from coverage.config import CoverageConfig +from coverage.config import read_coverage_config from coverage.data import CoverageData, CoverageDataFiles -from coverage.debug import DebugControl +from coverage.debug import DebugControl, write_formatted_info from coverage.files import TreeMatcher, FnmatchMatcher from coverage.files import PathAliases, find_python_files, prep_patterns +from coverage.files import canonical_filename, set_relative_directory from coverage.files import ModuleMatcher, abs_file from coverage.html import HtmlReporter from coverage.misc import CoverageException, bool_or_none, join_regex from coverage.misc import file_be_gone, isolate_module -from coverage.multiproc import patch_multiprocessing from coverage.plugin import FileReporter from coverage.plugin_support import Plugins -from coverage.python import PythonFileReporter +from coverage.python import PythonFileReporter, source_for_file from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter +try: + from coverage.multiproc import patch_multiprocessing +except ImportError: # pragma: only jython + # Jython has no multiprocessing module. + patch_multiprocessing = None + os = isolate_module(os) # Pypy has some unusual stuff in the "stdlib". Consider those locations -# when deciding where the stdlib is. This modules are not used for anything, +# when deciding where the stdlib is. These modules are not used for anything, # they are modules importable from the pypy lib directories, so that we can # find those directories. _structseq = _pypy_irc_topic = None @@ -101,8 +109,8 @@ class Coverage(object): file can't be read, it is an error. * If it is True, then a few standard files names are tried - (".coveragerc", "setup.cfg"). It is not an error for these files - to not be found. + (".coveragerc", "setup.cfg", "tox.ini"). It is not an error for + these files to not be found. * If it is False, then no configuration file is read. @@ -130,49 +138,18 @@ class Coverage(object): The `concurrency` parameter can now be a list of strings. """ - # Build our configuration from a number of sources: - # 1: defaults: - self.config = CoverageConfig() - - # 2: from the rcfile, .coveragerc or setup.cfg file: - if config_file: - # pylint: disable=redefined-variable-type - did_read_rc = False - # Some API users were specifying ".coveragerc" to mean the same as - # True, so make it so. - if config_file == ".coveragerc": - config_file = True - specified_file = (config_file is not True) - if not specified_file: - config_file = ".coveragerc" - self.config_file = config_file - - did_read_rc = self.config.from_file(config_file) - - if not did_read_rc: - if specified_file: - raise CoverageException( - "Couldn't read '%s' as a config file" % config_file - ) - self.config.from_file("setup.cfg", section_prefix="coverage:") - - # 3: from environment variables: - env_data_file = os.environ.get('COVERAGE_FILE') - if env_data_file: - self.config.data_file = env_data_file - debugs = os.environ.get('COVERAGE_DEBUG') - if debugs: - self.config.debug.extend(debugs.split(",")) - - # 4: from constructor arguments: - self.config.from_args( + # Build our configuration from a number of sources. + self.config_file, self.config = read_coverage_config( + config_file=config_file, data_file=data_file, cover_pylib=cover_pylib, timid=timid, branch=branch, parallel=bool_or_none(data_suffix), source=source, omit=omit, include=include, debug=debug, concurrency=concurrency, ) + # This is injectable by tests. self._debug_file = None + self._auto_load = self._auto_save = auto_data self._data_suffix = data_suffix @@ -191,10 +168,11 @@ class Coverage(object): # Other instance attributes, set later. self.omit = self.include = self.source = None + self.source_pkgs_unmatched = None self.source_pkgs = None self.data = self.data_files = self.collector = None self.plugins = None - self.pylib_dirs = self.cover_dirs = None + self.pylib_paths = self.cover_paths = None self.data_suffix = self.run_suffix = None self._exclude_re = None self.debug = None @@ -204,8 +182,6 @@ class Coverage(object): self._inited = False # Have we started collecting and not stopped it? self._started = False - # Have we measured some data and not harvested it? - self._measured = False # If we have sub-process measurement happening automatically, then we # want any explicit creation of a Coverage object to mean, this process @@ -226,6 +202,8 @@ class Coverage(object): if self._inited: return + self._inited = True + # Create and configure the debugging controller. COVERAGE_DEBUG_FILE # is an environment variable, the name of a file to append debug logs # to. @@ -245,22 +223,27 @@ class Coverage(object): self._exclude_re = {} self._exclude_regex_stale() - files.set_relative_directory() + set_relative_directory() # The source argument can be directories or package names. self.source = [] self.source_pkgs = [] for src in self.config.source or []: - if os.path.exists(src): - self.source.append(files.canonical_filename(src)) + if os.path.isdir(src): + self.source.append(canonical_filename(src)) else: self.source_pkgs.append(src) + self.source_pkgs_unmatched = self.source_pkgs[:] self.omit = prep_patterns(self.config.omit) self.include = prep_patterns(self.config.include) concurrency = self.config.concurrency or [] if "multiprocessing" in concurrency: + if not patch_multiprocessing: + raise CoverageException( # pragma: only jython + "multiprocessing is not supported on this Python" + ) patch_multiprocessing(rcfile=self.config_file) # Multi-processing uses parallel for the subprocesses, so also use # it for the main process. @@ -306,10 +289,12 @@ class Coverage(object): # data file will be written into the directory where the process # started rather than wherever the process eventually chdir'd to. self.data = CoverageData(debug=self.debug) - self.data_files = CoverageDataFiles(basename=self.config.data_file, warn=self._warn) + self.data_files = CoverageDataFiles( + basename=self.config.data_file, warn=self._warn, debug=self.debug, + ) # The directories for files considered "installed with the interpreter". - self.pylib_dirs = set() + self.pylib_paths = set() if not self.config.cover_pylib: # Look at where some standard modules are located. That's the # indication for "installed with the interpreter". In some @@ -318,7 +303,7 @@ class Coverage(object): # we've imported, and take all the different ones. for m in (atexit, inspect, os, platform, _pypy_irc_topic, re, _structseq, traceback): if m is not None and hasattr(m, "__file__"): - self.pylib_dirs.add(self._canonical_dir(m)) + self.pylib_paths.add(self._canonical_path(m, directory=True)) if _structseq and not hasattr(_structseq, '__file__'): # PyPy 2.4 has no __file__ in the builtin modules, but the code @@ -329,96 +314,77 @@ class Coverage(object): structseq_file = structseq_new.func_code.co_filename except AttributeError: structseq_file = structseq_new.__code__.co_filename - self.pylib_dirs.add(self._canonical_dir(structseq_file)) + self.pylib_paths.add(self._canonical_path(structseq_file)) # To avoid tracing the coverage.py code itself, we skip anything # located where we are. - self.cover_dirs = [self._canonical_dir(__file__)] + self.cover_paths = [self._canonical_path(__file__, directory=True)] if env.TESTING: + # Don't include our own test code. + self.cover_paths.append(os.path.join(self.cover_paths[0], "tests")) + # When testing, we use PyContracts, which should be considered # part of coverage.py, and it uses six. Exclude those directories # just as we exclude ourselves. import contracts import six for mod in [contracts, six]: - self.cover_dirs.append(self._canonical_dir(mod)) + self.cover_paths.append(self._canonical_path(mod)) # Set the reporting precision. Numbers.set_precision(self.config.precision) atexit.register(self._atexit) - self._inited = True - # Create the matchers we need for _should_trace if self.source or self.source_pkgs: self.source_match = TreeMatcher(self.source) self.source_pkgs_match = ModuleMatcher(self.source_pkgs) else: - if self.cover_dirs: - self.cover_match = TreeMatcher(self.cover_dirs) - if self.pylib_dirs: - self.pylib_match = TreeMatcher(self.pylib_dirs) + if self.cover_paths: + self.cover_match = TreeMatcher(self.cover_paths) + if self.pylib_paths: + self.pylib_match = TreeMatcher(self.pylib_paths) if self.include: self.include_match = FnmatchMatcher(self.include) if self.omit: self.omit_match = FnmatchMatcher(self.omit) # The user may want to debug things, show info if desired. + self._write_startup_debug() + + def _write_startup_debug(self): + """Write out debug info at startup if needed.""" wrote_any = False with self.debug.without_callers(): if self.debug.should('config'): config_info = sorted(self.config.__dict__.items()) - self.debug.write_formatted_info("config", config_info) + write_formatted_info(self.debug, "config", config_info) wrote_any = True if self.debug.should('sys'): - self.debug.write_formatted_info("sys", self.sys_info()) + write_formatted_info(self.debug, "sys", self.sys_info()) for plugin in self.plugins: header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() - self.debug.write_formatted_info(header, info) + write_formatted_info(self.debug, header, info) wrote_any = True if wrote_any: - self.debug.write_formatted_info("end", ()) + write_formatted_info(self.debug, "end", ()) - def _canonical_dir(self, morf): - """Return the canonical directory of the module or file `morf`.""" - morf_filename = PythonFileReporter(morf, self).filename - return os.path.split(morf_filename)[0] + def _canonical_path(self, morf, directory=False): + """Return the canonical path of the module or file `morf`. - def _source_for_file(self, filename): - """Return the source file for `filename`. - - Given a file name being traced, return the best guess as to the source - file to attribute it to. + If the module is a package, then return its directory. If it is a + module, then return its file, unless `directory` is True, in which + case return its enclosing directory. """ - if filename.endswith(".py"): - # .py files are themselves source files. - return filename - - elif filename.endswith((".pyc", ".pyo")): - # Bytecode files probably have source files near them. - py_filename = filename[:-1] - if os.path.exists(py_filename): - # Found a .py file, use that. - return py_filename - if env.WINDOWS: - # On Windows, it could be a .pyw file. - pyw_filename = py_filename + "w" - if os.path.exists(pyw_filename): - return pyw_filename - # Didn't find source, but it's probably the .py file we want. - return py_filename - - elif filename.endswith("$py.class"): - # Jython is easy to guess. - return filename[:-9] + ".py" - - # No idea, just use the file name as-is. - return filename + morf_path = PythonFileReporter(morf, self).filename + if morf_path.endswith("__init__.py") or directory: + morf_path = os.path.split(morf_path)[0] + return morf_path def _name_for_module(self, module_globals, filename): """Get the name of the module for a set of globals and file name. @@ -432,6 +398,10 @@ class Coverage(object): can't be determined, None is returned. """ + if module_globals is None: # pragma: only ironpython + # IronPython doesn't provide globals: https://github.com/IronLanguages/main/issues/1296 + module_globals = {} + dunder_name = module_globals.get('__name__', None) if isinstance(dunder_name, str) and dunder_name != '__main__': @@ -480,9 +450,9 @@ class Coverage(object): # .pyc files can be moved after compilation (for example, by being # installed), we look for __file__ in the frame and prefer it to the # co_filename value. - dunder_file = frame.f_globals.get('__file__') + dunder_file = frame.f_globals and frame.f_globals.get('__file__') if dunder_file: - filename = self._source_for_file(dunder_file) + filename = source_for_file(dunder_file) if original_filename and not original_filename.startswith('<'): orig = os.path.basename(original_filename) if orig != os.path.basename(filename): @@ -514,7 +484,7 @@ class Coverage(object): if filename.endswith("$py.class"): filename = filename[:-9] + ".py" - canonical = files.canonical_filename(filename) + canonical = canonical_filename(filename) disp.canonical_filename = canonical # Try the plugins, see if they have an opinion about the file. @@ -532,7 +502,7 @@ class Coverage(object): if file_tracer.has_dynamic_source_filename(): disp.has_dynamic_filename = True else: - disp.source_filename = files.canonical_filename( + disp.source_filename = canonical_filename( file_tracer.source_filename() ) break @@ -579,8 +549,8 @@ class Coverage(object): # stdlib and coverage.py directories. if self.source_match: if self.source_pkgs_match.match(modulename): - if modulename in self.source_pkgs: - self.source_pkgs.remove(modulename) + if modulename in self.source_pkgs_unmatched: + self.source_pkgs_unmatched.remove(modulename) return None # There's no reason to skip this file. if not self.source_match.match(filename): @@ -633,9 +603,18 @@ class Coverage(object): return not reason - def _warn(self, msg): - """Use `msg` as a warning.""" + def _warn(self, msg, slug=None): + """Use `msg` as a warning. + + For warning suppression, use `slug` as the shorthand. + """ + if slug in self.config.disable_warnings: + # Don't issue the warning + return + self._warnings.append(msg) + if slug: + msg = "%s (%s)" % (msg, slug) if self.debug.should('pid'): msg = "[%d] %s" % (os.getpid(), msg) sys.stderr.write("Coverage.py warning: %s\n" % msg) @@ -694,7 +673,7 @@ class Coverage(object): def start(self): """Start measuring code coverage. - Coverage measurement actually occurs in functions called after + Coverage measurement only occurs in functions called after :meth:`start` is invoked. Statements in the same scope as :meth:`start` won't be measured. @@ -712,7 +691,6 @@ class Coverage(object): self.collector.start() self._started = True - self._measured = True def stop(self): """Stop measuring code coverage.""" @@ -722,8 +700,8 @@ class Coverage(object): def _atexit(self): """Clean up on process shutdown.""" - if self.debug and self.debug.should('dataio'): - self.debug.write("Inside _atexit: self._auto_save = %r" % (self._auto_save,)) + if self.debug.should("process"): + self.debug.write("atexit: {0!r}".format(self)) if self._started: self.stop() if self._auto_save: @@ -832,7 +810,7 @@ class Coverage(object): ) def get_data(self): - """Get the collected data and reset the collector. + """Get the collected data. Also warn about various problems collecting data. @@ -842,46 +820,78 @@ class Coverage(object): """ self._init() - if not self._measured: - return self.data - self.collector.save_data(self.data) + if self.collector.save_data(self.data): + self._post_save_work() + + return self.data + + def _post_save_work(self): + """After saving data, look for warnings, post-work, etc. - # If there are still entries in the source_pkgs list, then we never - # encountered those packages. + Warn about things that should have happened but didn't. + Look for unexecuted files. + + """ + # If there are still entries in the source_pkgs_unmatched list, + # then we never encountered those packages. if self._warn_unimported_source: - for pkg in self.source_pkgs: + for pkg in self.source_pkgs_unmatched: if pkg not in sys.modules: - self._warn("Module %s was never imported." % pkg) + self._warn("Module %s was never imported." % pkg, slug="module-not-imported") elif not ( hasattr(sys.modules[pkg], '__file__') and os.path.exists(sys.modules[pkg].__file__) ): - self._warn("Module %s has no Python source." % pkg) + self._warn("Module %s has no Python source." % pkg, slug="module-not-python") else: - self._warn("Module %s was previously imported, but not measured." % pkg) + self._warn( + "Module %s was previously imported, but not measured." % pkg, + slug="module-not-measured", + ) # Find out if we got any data. if not self.data and self._warn_no_data: - self._warn("No data was collected.") + self._warn("No data was collected.", slug="no-data-collected") # Find files that were never executed at all. - for src in self.source: - for py_file in find_python_files(src): - py_file = files.canonical_filename(py_file) - - if self.omit_match and self.omit_match.match(py_file): - # Turns out this file was omitted, so don't pull it back - # in as unexecuted. - continue + for pkg in self.source_pkgs: + if (not pkg in sys.modules or + not hasattr(sys.modules[pkg], '__file__') or + not os.path.exists(sys.modules[pkg].__file__)): + continue + pkg_file = source_for_file(sys.modules[pkg].__file__) + self._find_unexecuted_files(self._canonical_path(pkg_file)) - self.data.touch_file(py_file) + for src in self.source: + self._find_unexecuted_files(src) if self.config.note: self.data.add_run_info(note=self.config.note) - self._measured = False - return self.data + def _find_plugin_files(self, src_dir): + """Get executable files from the plugins.""" + for plugin in self.plugins: + for x_file in plugin.find_executable_files(src_dir): + yield x_file, plugin._coverage_plugin_name + + def _find_unexecuted_files(self, src_dir): + """Find unexecuted files in `src_dir`. + + Search for files in `src_dir` that are probably importable, + and add them as unexecuted files in `self.data`. + + """ + py_files = ((py_file, None) for py_file in find_python_files(src_dir)) + plugin_files = self._find_plugin_files(src_dir) + + for file_path, plugin_name in itertools.chain(py_files, plugin_files): + file_path = canonical_filename(file_path) + if self.omit_match and self.omit_match.match(file_path): + # Turns out this file was omitted, so don't pull it back + # in as unexecuted. + continue + self.data.touch_file(file_path, plugin_name) # Backward compatibility with version 1. def analysis(self, morf): @@ -949,7 +959,6 @@ class Coverage(object): ) if file_reporter == "python": - # pylint: disable=redefined-variable-type file_reporter = PythonFileReporter(morf, self) return file_reporter @@ -993,6 +1002,8 @@ class Coverage(object): included in the report. Files matching `omit` will not be included in the report. + If `skip_covered` is True, don't report on files with 100% coverage. + Returns a float, the total percentage covered. """ @@ -1026,7 +1037,8 @@ class Coverage(object): reporter.report(morfs, directory=directory) def html_report(self, morfs=None, directory=None, ignore_errors=None, - omit=None, include=None, extra_css=None, title=None): + omit=None, include=None, extra_css=None, title=None, + skip_covered=None): """Generate an HTML report. The HTML is written to `directory`. The file "index.html" is the @@ -1048,6 +1060,7 @@ class Coverage(object): self.config.from_args( ignore_errors=ignore_errors, omit=omit, include=include, html_dir=directory, extra_css=extra_css, html_title=title, + skip_covered=skip_covered, ) reporter = HtmlReporter(self, self.config) return reporter.report(morfs) @@ -1120,8 +1133,8 @@ class Coverage(object): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_dirs', self.cover_dirs), - ('pylib_dirs', self.pylib_dirs), + ('cover_paths', self.cover_paths), + ('pylib_paths', self.pylib_paths), ('tracer', self.collector.tracer_name()), ('plugins.file_tracers', ft_plugins), ('config_files', self.config.attempted_config_files), diff --git a/coverage/ctracer/datastack.c b/coverage/ctracer/datastack.c index 5a384e6..515ba92 100644 --- a/coverage/ctracer/datastack.c +++ b/coverage/ctracer/datastack.c @@ -4,7 +4,7 @@ #include "util.h" #include "datastack.h" -#define STACK_DELTA 100 +#define STACK_DELTA 20 int DataStack_init(Stats *pstats, DataStack *pdata_stack) @@ -18,6 +18,11 @@ DataStack_init(Stats *pstats, DataStack *pdata_stack) void DataStack_dealloc(Stats *pstats, DataStack *pdata_stack) { + int i; + + for (i = 0; i < pdata_stack->alloc; i++) { + Py_XDECREF(pdata_stack->stack[i].file_data); + } PyMem_Free(pdata_stack->stack); } @@ -35,6 +40,9 @@ DataStack_grow(Stats *pstats, DataStack *pdata_stack) pdata_stack->depth--; return RET_ERROR; } + /* Zero the new entries. */ + memset(bigger_data_stack + pdata_stack->alloc, 0, STACK_DELTA * sizeof(DataStackEntry)); + pdata_stack->stack = bigger_data_stack; pdata_stack->alloc = bigger; } diff --git a/coverage/ctracer/datastack.h b/coverage/ctracer/datastack.h index b63af2c..b2dbeb9 100644 --- a/coverage/ctracer/datastack.h +++ b/coverage/ctracer/datastack.h @@ -9,18 +9,16 @@ /* An entry on the data stack. For each call frame, we need to record all * the information needed for CTracer_handle_line to operate as quickly as - * possible. All PyObject* here are borrowed references. + * possible. */ typedef struct DataStackEntry { - /* The current file_data dictionary. Borrowed, owned by self->data. */ + /* The current file_data dictionary. Owned. */ PyObject * file_data; - /* The disposition object for this frame. If collector.py and control.py - * are working properly, this will be an instance of CFileDisposition. - */ + /* The disposition object for this frame. A borrowed instance of CFileDisposition. */ PyObject * disposition; - /* The FileTracer handling this frame, or None if it's Python. */ + /* The FileTracer handling this frame, or None if it's Python. Borrowed. */ PyObject * file_tracer; /* The line number of the last line recorded, for tracing arcs. diff --git a/coverage/ctracer/stats.h b/coverage/ctracer/stats.h index a72117c..c5ffdf5 100644 --- a/coverage/ctracer/stats.h +++ b/coverage/ctracer/stats.h @@ -19,7 +19,7 @@ typedef struct Stats { unsigned int returns; unsigned int exceptions; unsigned int others; - unsigned int new_files; + unsigned int files; unsigned int missed_returns; unsigned int stack_reallocs; unsigned int errors; diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index ac16b6b..095df11 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -71,9 +71,8 @@ CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused) self->pdata_stack = &self->data_stack; - self->cur_entry.last_line = -1; - self->context = Py_None; + Py_INCREF(self->context); ret = RET_OK; goto ok; @@ -168,7 +167,7 @@ showlog(int depth, int lineno, PyObject * filename, const char * msg) static const char * what_sym[] = {"CALL", "EXC ", "LINE", "RET "}; #endif -/* Record a pair of integers in self->cur_entry.file_data. */ +/* Record a pair of integers in self->pcur_entry->file_data. */ static int CTracer_record_pair(CTracer *self, int l1, int l2) { @@ -181,7 +180,7 @@ CTracer_record_pair(CTracer *self, int l1, int l2) goto error; } - if (PyDict_SetItem(self->cur_entry.file_data, t, Py_None) < 0) { + if (PyDict_SetItem(self->pcur_entry->file_data, t, Py_None) < 0) { goto error; } @@ -300,14 +299,14 @@ CTracer_check_missing_return(CTracer *self, PyFrameObject *frame) goto error; } if (self->pdata_stack->depth >= 0) { - if (self->tracing_arcs && self->cur_entry.file_data) { - if (CTracer_record_pair(self, self->cur_entry.last_line, -self->last_exc_firstlineno) < 0) { + if (self->tracing_arcs && self->pcur_entry->file_data) { + if (CTracer_record_pair(self, self->pcur_entry->last_line, -self->last_exc_firstlineno) < 0) { goto error; } } SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn"); - self->cur_entry = self->pdata_stack->stack[self->pdata_stack->depth]; self->pdata_stack->depth--; + self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } } self->last_exc_back = NULL; @@ -341,8 +340,8 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) CFileDisposition * pdisp = NULL; - STATS( self->stats.calls++; ) + self->activity = TRUE; /* Grow the stack. */ if (CTracer_set_pdata_stack(self) < 0) { @@ -351,9 +350,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) if (DataStack_grow(&self->stats, self->pdata_stack) < 0) { goto error; } - - /* Push the current state on the stack. */ - self->pdata_stack->stack[self->pdata_stack->depth] = self->cur_entry; + self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; /* See if this frame begins a new context. */ if (self->should_start_context && self->context == Py_None) { @@ -369,7 +366,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) PyObject * val; Py_DECREF(self->context); self->context = context; - self->cur_entry.started_context = TRUE; + self->pcur_entry->started_context = TRUE; STATS( self->stats.pycalls++; ) val = PyObject_CallFunctionObjArgs(self->switch_context, context, NULL); if (val == NULL) { @@ -379,11 +376,11 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) } else { Py_DECREF(context); - self->cur_entry.started_context = FALSE; + self->pcur_entry->started_context = FALSE; } } else { - self->cur_entry.started_context = FALSE; + self->pcur_entry->started_context = FALSE; } /* Check if we should trace this line. */ @@ -393,7 +390,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) if (PyErr_Occurred()) { goto error; } - STATS( self->stats.new_files++; ) + STATS( self->stats.files++; ) /* We've never considered this file before. */ /* Ask should_trace about it. */ @@ -474,7 +471,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) if (PyErr_Occurred()) { goto error; } - STATS( self->stats.new_files++; ) + STATS( self->stats.files++; ) STATS( self->stats.pycalls++; ) should_include_bool = PyObject_CallFunctionObjArgs(self->check_include, tracename, frame, NULL); if (should_include_bool == NULL) { @@ -511,7 +508,6 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) goto error; } ret2 = PyDict_SetItem(self->data, tracename, file_data); - Py_DECREF(file_data); if (ret2 < 0) { goto error; } @@ -524,32 +520,39 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) } } } + else { + /* PyDict_GetItem gives a borrowed reference. Own it. */ + Py_INCREF(file_data); + } - self->cur_entry.file_data = file_data; - self->cur_entry.file_tracer = file_tracer; + Py_XDECREF(self->pcur_entry->file_data); + self->pcur_entry->file_data = file_data; + self->pcur_entry->file_tracer = file_tracer; - /* Make the frame right in case settrace(gettrace()) happens. */ - Py_INCREF(self); - frame->f_trace = (PyObject*)self; SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "traced"); } else { - self->cur_entry.file_data = NULL; - self->cur_entry.file_tracer = Py_None; + Py_XDECREF(self->pcur_entry->file_data); + self->pcur_entry->file_data = NULL; + self->pcur_entry->file_tracer = Py_None; SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "skipped"); } - self->cur_entry.disposition = disposition; + self->pcur_entry->disposition = disposition; + + /* Make the frame right in case settrace(gettrace()) happens. */ + Py_INCREF(self); + My_XSETREF(frame->f_trace, (PyObject*)self); /* A call event is really a "start frame" event, and can happen for * re-entering a generator also. f_lasti is -1 for a true call, and a * real byte offset for a generator re-entry. */ if (frame->f_lasti < 0) { - self->cur_entry.last_line = -frame->f_code->co_firstlineno; + self->pcur_entry->last_line = -frame->f_code->co_firstlineno; } else { - self->cur_entry.last_line = frame->f_lineno; + self->pcur_entry->last_line = frame->f_lineno; } ok: @@ -673,22 +676,22 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) STATS( self->stats.lines++; ) if (self->pdata_stack->depth >= 0) { SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "line"); - if (self->cur_entry.file_data) { + if (self->pcur_entry->file_data) { int lineno_from = -1; int lineno_to = -1; /* We're tracing in this frame: record something. */ - if (self->cur_entry.file_tracer != Py_None) { + if (self->pcur_entry->file_tracer != Py_None) { PyObject * from_to = NULL; STATS( self->stats.pycalls++; ) - from_to = PyObject_CallMethodObjArgs(self->cur_entry.file_tracer, str_line_number_range, frame, NULL); + from_to = PyObject_CallMethodObjArgs(self->pcur_entry->file_tracer, str_line_number_range, frame, NULL); if (from_to == NULL) { goto error; } ret2 = CTracer_unpack_pair(self, from_to, &lineno_from, &lineno_to); Py_DECREF(from_to); if (ret2 < 0) { - CTracer_disable_plugin(self, self->cur_entry.disposition); + CTracer_disable_plugin(self, self->pcur_entry->disposition); goto ok; } } @@ -700,7 +703,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) for (; lineno_from <= lineno_to; lineno_from++) { if (self->tracing_arcs) { /* Tracing arcs: key is (last_line,this_line). */ - if (CTracer_record_pair(self, self->cur_entry.last_line, lineno_from) < 0) { + if (CTracer_record_pair(self, self->pcur_entry->last_line, lineno_from) < 0) { goto error; } } @@ -711,14 +714,14 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) goto error; } - ret2 = PyDict_SetItem(self->cur_entry.file_data, this_line, Py_None); + ret2 = PyDict_SetItem(self->pcur_entry->file_data, this_line, Py_None); Py_DECREF(this_line); if (ret2 < 0) { goto error; } } - self->cur_entry.last_line = lineno_from; + self->pcur_entry->last_line = lineno_from; } } } @@ -742,8 +745,10 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) if (CTracer_set_pdata_stack(self) < 0) { goto error; } + self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; + if (self->pdata_stack->depth >= 0) { - if (self->tracing_arcs && self->cur_entry.file_data) { + if (self->tracing_arcs && self->pcur_entry->file_data) { /* Need to distinguish between RETURN_VALUE and YIELD_VALUE. Read * the current bytecode to see what it is. In unusual circumstances * (Cython code), co_code can be the empty string, so range-check @@ -758,14 +763,14 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) } if (bytecode != YIELD_VALUE) { int first = frame->f_code->co_firstlineno; - if (CTracer_record_pair(self, self->cur_entry.last_line, -first) < 0) { + if (CTracer_record_pair(self, self->pcur_entry->last_line, -first) < 0) { goto error; } } } /* If this frame started a context, then returning from it ends the context. */ - if (self->cur_entry.started_context) { + if (self->pcur_entry->started_context) { PyObject * val; Py_DECREF(self->context); self->context = Py_None; @@ -781,8 +786,8 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) /* Pop the stack. */ SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "return"); - self->cur_entry = self->pdata_stack->stack[self->pdata_stack->depth]; self->pdata_stack->depth--; + self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } ret = RET_OK; @@ -824,6 +829,10 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse { int ret = RET_ERROR; + #if DO_NOTHING + return RET_OK; + #endif + #if WHAT_LOG || TRACE_LOG PyObject * ascii = NULL; #endif @@ -922,6 +931,10 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) PyObject *ret = NULL; PyObject * ascii = NULL; + #if DO_NOTHING + CRASH + #endif + static char *what_names[] = { "call", "exception", "line", "return", "c_call", "c_exception", "c_return", @@ -1022,7 +1035,25 @@ CTracer_stop(CTracer *self, PyObject *args_unused) } static PyObject * -CTracer_get_stats(CTracer *self) +CTracer_activity(CTracer *self, PyObject *args_unused) +{ + if (self->activity) { + Py_RETURN_TRUE; + } + else { + Py_RETURN_FALSE; + } +} + +static PyObject * +CTracer_reset_activity(CTracer *self, PyObject *args_unused) +{ + self->activity = FALSE; + Py_RETURN_NONE; +} + +static PyObject * +CTracer_get_stats(CTracer *self, PyObject *args_unused) { #if COLLECT_STATS return Py_BuildValue( @@ -1032,7 +1063,7 @@ CTracer_get_stats(CTracer *self) "returns", self->stats.returns, "exceptions", self->stats.exceptions, "others", self->stats.others, - "new_files", self->stats.new_files, + "files", self->stats.files, "missed_returns", self->stats.missed_returns, "stack_reallocs", self->stats.stack_reallocs, "stack_alloc", self->pdata_stack->alloc, @@ -1075,7 +1106,7 @@ CTracer_members[] = { PyDoc_STR("Function for starting contexts.") }, { "switch_context", T_OBJECT, offsetof(CTracer, switch_context), 0, - PyDoc_STR("Function for switch to a new context.") }, + PyDoc_STR("Function for switching to a new context.") }, { NULL } }; @@ -1091,6 +1122,12 @@ CTracer_methods[] = { { "get_stats", (PyCFunction) CTracer_get_stats, METH_VARARGS, PyDoc_STR("Get statistics about the tracing") }, + { "activity", (PyCFunction) CTracer_activity, METH_VARARGS, + PyDoc_STR("Has there been any activity?") }, + + { "reset_activity", (PyCFunction) CTracer_reset_activity, METH_VARARGS, + PyDoc_STR("Reset the activity flag") }, + { NULL } }; diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h index 438317b..d5d630f 100644 --- a/coverage/ctracer/tracer.h +++ b/coverage/ctracer/tracer.h @@ -33,6 +33,8 @@ typedef struct CTracer { BOOL started; /* Are we tracing arcs, or just lines? */ BOOL tracing_arcs; + /* Have we had any activity? */ + BOOL activity; /* The data stack is a stack of dictionaries. Each dictionary collects @@ -54,8 +56,8 @@ typedef struct CTracer { int data_stacks_used; DataStack * pdata_stack; - /* The current file's data stack entry, copied from the stack. */ - DataStackEntry cur_entry; + /* The current file's data stack entry. */ + DataStackEntry * pcur_entry; /* The parent frame for the last exception event, to fix missing returns. */ PyFrameObject * last_exc_back; diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h index cafcc28..f0c302c 100644 --- a/coverage/ctracer/util.h +++ b/coverage/ctracer/util.h @@ -10,6 +10,7 @@ #undef WHAT_LOG /* Define to log the WHAT params in the trace function. */ #undef TRACE_LOG /* Define to log our bookkeeping. */ #undef COLLECT_STATS /* Collect counters: stats are printed when tracer is stopped. */ +#undef DO_NOTHING /* Define this to make the tracer do nothing. */ /* Py 2.x and 3.x compatibility */ @@ -43,6 +44,14 @@ #endif /* Py3k */ +// Undocumented, and not in 2.6, so our own copy of it. +#define My_XSETREF(op, op2) \ + do { \ + PyObject *_py_tmp = (PyObject *)(op); \ + (op) = (op2); \ + Py_XDECREF(_py_tmp); \ + } while (0) + /* The values returned to indicate ok or error. */ #define RET_OK 0 #define RET_ERROR -1 @@ -52,4 +61,7 @@ typedef int BOOL; #define FALSE 0 #define TRUE 1 +/* Only for extreme machete-mode debugging! */ +#define CRASH { printf("*** CRASH! ***\n"); *((int*)1) = 1; } + #endif /* _COVERAGE_UTIL_H */ diff --git a/coverage/data.py b/coverage/data.py index 95b6888..ecfb86b 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -144,9 +144,6 @@ class CoverageData(object): # A list of dicts of information about the coverage.py runs. self._runs = [] - if self._debug and self._debug.should('dataio'): - self._debug.write("Creating CoverageData object") - def __repr__(self): return "<{klass} lines={lines} arcs={arcs} tracers={tracers} runs={runs}>".format( klass=self.__class__.__name__, @@ -419,8 +416,12 @@ class CoverageData(object): self._runs[0].update(kwargs) self._validate() - def touch_file(self, filename): - """Ensure that `filename` appears in the data, empty if needed.""" + def touch_file(self, filename, plugin_name=""): + """Ensure that `filename` appears in the data, empty if needed. + + `plugin_name` is the name of the plugin resposible for this file. It is used + to associate the right filereporter, etc. + """ if self._debug and self._debug.should('dataop'): self._debug.write("Touching %r" % (filename,)) if not self._has_arcs() and not self._has_lines(): @@ -431,6 +432,9 @@ class CoverageData(object): else: where = self._lines where.setdefault(filename, []) + if plugin_name: + # Set the tracer for this file + self._file_tracers[filename] = plugin_name self._validate() @@ -608,15 +612,19 @@ class CoverageData(object): class CoverageDataFiles(object): """Manage the use of coverage data files.""" - def __init__(self, basename=None, warn=None): + def __init__(self, basename=None, warn=None, debug=None): """Create a CoverageDataFiles to manage data files. `warn` is the warning function to use. `basename` is the name of the file to use for storing data. + `debug` is a `DebugControl` object for writing debug messages. + """ self.warn = warn + self.debug = debug + # Construct the file name that will be used for data storage. self.filename = os.path.abspath(basename or ".coverage") @@ -627,12 +635,16 @@ class CoverageDataFiles(object): basename by parallel-mode. """ + if self.debug and self.debug.should('dataio'): + self.debug.write("Erasing data file %r" % (self.filename,)) file_be_gone(self.filename) if parallel: data_dir, local = os.path.split(self.filename) localdot = local + '.*' pattern = os.path.join(os.path.abspath(data_dir), localdot) for filename in glob.glob(pattern): + if self.debug and self.debug.should('dataio'): + self.debug.write("Erasing parallel data file %r" % (filename,)) file_be_gone(filename) def read(self, data): @@ -660,7 +672,7 @@ class CoverageDataFiles(object): with open(_TEST_NAME_FILE) as f: test_name = f.read() extra = "." + test_name - dice = random.Random().randint(0, 999999) + dice = random.Random(os.urandom(8)).randint(0, 999999) suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) if suffix: @@ -711,7 +723,7 @@ class CoverageDataFiles(object): raise CoverageException("No data to combine") for f in files_to_combine: - new_data = CoverageData() + new_data = CoverageData(debug=self.debug) try: new_data.read_file(f) except CoverageException as exc: @@ -721,6 +733,8 @@ class CoverageDataFiles(object): self.warn(str(exc)) else: data.update(new_data, aliases=aliases) + if self.debug and self.debug.should('dataio'): + self.debug.write("Deleting combined data file %r" % (f,)) file_be_gone(f) diff --git a/coverage/debug.py b/coverage/debug.py index dff8beb..e68736f 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -8,7 +8,12 @@ import inspect import os import re import sys +try: + import _thread +except ImportError: + import thread as _thread +from coverage.backward import StringIO from coverage.misc import isolate_module os = isolate_module(os) @@ -28,18 +33,27 @@ class DebugControl(object): def __init__(self, options, output): """Configure the options and output file for debugging.""" - self.options = options - self.output = output + self.options = list(options) + FORCED_DEBUG + self.raw_output = output self.suppress_callers = False + filters = [] + if self.should('pid'): + filters.append(add_pid_and_tid) + self.output = DebugOutputFile( + self.raw_output, + show_process=self.should('process'), + filters=filters, + ) + def __repr__(self): - return "<DebugControl options=%r output=%r>" % (self.options, self.output) + return "<DebugControl options=%r raw_output=%r>" % (self.options, self.raw_output) def should(self, option): """Decide whether to output debug information in category `option`.""" if option == "callers" and self.suppress_callers: return False - return (option in self.options or option in FORCED_DEBUG) + return (option in self.options) @contextlib.contextmanager def without_callers(self): @@ -57,18 +71,20 @@ class DebugControl(object): `msg` is the line to write. A newline will be appended. """ - if self.should('pid'): - msg = "pid %5d: %s" % (os.getpid(), msg) self.output.write(msg+"\n") if self.should('callers'): dump_stack_frames(out=self.output, skip=1) self.output.flush() - def write_formatted_info(self, header, info): - """Write a sequence of (label,data) pairs nicely.""" - self.write(info_header(header)) - for line in info_formatter(info): - self.write(" %s" % line) + +class DebugControlString(DebugControl): + """A `DebugControl` that writes to a StringIO, for testing.""" + def __init__(self, options): + super(DebugControlString, self).__init__(options, StringIO()) + + def get_output(self): + """Get the output text from the `DebugControl`.""" + return self.raw_output.getvalue() def info_header(label): @@ -99,6 +115,13 @@ def info_formatter(info): yield "%*s: %s" % (label_len, label, data) +def write_formatted_info(writer, header, info): + """Write a sequence of (label,data) pairs nicely.""" + writer.write(info_header(header)) + for line in info_formatter(info): + writer.write(" %s" % line) + + def short_stack(limit=None, skip=0): """Return a string summarizing the call stack. @@ -122,18 +145,122 @@ def short_stack(limit=None, skip=0): def dump_stack_frames(limit=None, out=None, skip=0): - """Print a summary of the stack to stdout, or some place else.""" + """Print a summary of the stack to stdout, or someplace else.""" out = out or sys.stdout out.write(short_stack(limit=limit, skip=skip+1)) out.write("\n") +def short_id(id64): + """Given a 64-bit id, make a shorter 16-bit one.""" + id16 = 0 + for offset in range(0, 64, 16): + id16 ^= id64 >> offset + return id16 & 0xFFFF + + +def add_pid_and_tid(text): + """A filter to add pid and tid to debug messages.""" + # Thread ids are useful, but too long. Make a shorter one. + tid = "{0:04x}".format(short_id(_thread.get_ident())) + text = "{0:5d}.{1}: {2}".format(os.getpid(), tid, text) + return text + + +def filter_text(text, filters): + """Run `text` through a series of filters. + + `filters` is a list of functions. Each takes a string and returns a + string. Each is run in turn. + + Returns: the final string that results after all of the filters have + run. + + """ + clean_text = text.rstrip() + ending = text[len(clean_text):] + text = clean_text + for fn in filters: + lines = [] + for line in text.splitlines(): + lines.extend(fn(line).splitlines()) + text = "\n".join(lines) + return text + ending + + +class CwdTracker(object): # pragma: debugging + """A class to add cwd info to debug messages.""" + def __init__(self): + self.cwd = None + + def filter(self, text): + """Add a cwd message for each new cwd.""" + cwd = os.getcwd() + if cwd != self.cwd: + text = "cwd is now {0!r}\n".format(cwd) + text + self.cwd = cwd + return text + + +class DebugOutputFile(object): # pragma: debugging + """A file-like object that includes pid and cwd information.""" + def __init__(self, outfile, show_process, filters): + self.outfile = outfile + self.show_process = show_process + self.filters = list(filters) + + if self.show_process: + self.filters.append(CwdTracker().filter) + cmd = " ".join(getattr(sys, 'argv', ['???'])) + self.write("New process: executable: %s\n" % (sys.executable,)) + self.write("New process: cmd: %s\n" % (cmd,)) + if hasattr(os, 'getppid'): + self.write("New process: parent pid: %s\n" % (os.getppid(),)) + + SYS_MOD_NAME = '$coverage.debug.DebugOutputFile.the_one' + + @classmethod + def the_one(cls, fileobj=None, show_process=True, filters=()): + """Get the process-wide singleton DebugOutputFile. + + If it doesn't exist yet, then create it as a wrapper around the file + object `fileobj`. `show_process` controls whether the debug file adds + process-level information. + + """ + # Because of the way igor.py deletes and re-imports modules, + # this class can be defined more than once. But we really want + # a process-wide singleton. So stash it in sys.modules instead of + # on a class attribute. Yes, this is aggressively gross. + the_one = sys.modules.get(cls.SYS_MOD_NAME) + if the_one is None: + assert fileobj is not None + sys.modules[cls.SYS_MOD_NAME] = the_one = cls(fileobj, show_process, filters) + return the_one + + def write(self, text): + """Just like file.write, but filter through all our filters.""" + self.outfile.write(filter_text(text, self.filters)) + self.outfile.flush() + + def flush(self): + """Flush our file.""" + self.outfile.flush() + + def log(msg, stack=False): # pragma: debugging """Write a log message as forcefully as possible.""" - with open("/tmp/covlog.txt", "a") as f: - f.write("{pid}: {msg}\n".format(pid=os.getpid(), msg=msg)) - if stack: - dump_stack_frames(out=f, skip=1) + out = DebugOutputFile.the_one() + out.write(msg+"\n") + if stack: + dump_stack_frames(out=out, skip=1) + + +def filter_aspectlib_frames(text): # pragma: debugging + """Aspectlib prints stack traces, but includes its own frames. Scrub those out.""" + # <<< aspectlib/__init__.py:257:function_wrapper < igor.py:143:run_tests < ... + text = re.sub(r"(?<= )aspectlib/[^.]+\.py:\d+:\w+ < ", "", text) + return text def enable_aspectlib_maybe(): # pragma: debugging @@ -142,7 +269,9 @@ def enable_aspectlib_maybe(): # pragma: debugging Define COVERAGE_ASPECTLIB to enable and configure aspectlib to trace execution:: - COVERAGE_ASPECTLIB=covaspect.txt:coverage.Coverage:coverage.data.CoverageData program... + $ export COVERAGE_LOG=covaspect.txt + $ export COVERAGE_ASPECTLIB=coverage.Coverage:coverage.data.CoverageData + $ coverage run blah.py ... This will trace all the public methods on Coverage and CoverageData, writing the information to covaspect.txt. @@ -155,28 +284,12 @@ def enable_aspectlib_maybe(): # pragma: debugging import aspectlib # pylint: disable=import-error import aspectlib.debug # pylint: disable=import-error - class AspectlibOutputFile(object): - """A file-like object that includes pid and cwd information.""" - def __init__(self, outfile): - self.outfile = outfile - self.cwd = None - - def write(self, text): - """Just like file.write""" - cwd = os.getcwd() - if cwd != self.cwd: - self._write("cwd is now {0!r}\n".format(cwd)) - self.cwd = cwd - self._write(text) - - def _write(self, text): - """The raw text-writer, so that we can use it ourselves.""" - self.outfile.write("{0:5d}: {1}".format(os.getpid(), text)) - - aspects = aspects.split(':') - aspects_file = AspectlibOutputFile(open(aspects[0], "a")) - aspect_log = aspectlib.debug.log(print_to=aspects_file, use_logging=False) - aspects = aspects[1:] + filename = os.environ.get("COVERAGE_LOG", "/tmp/covlog.txt") + filters = [add_pid_and_tid, filter_aspectlib_frames] + aspects_file = DebugOutputFile.the_one(open(filename, "a"), show_process=True, filters=filters) + aspect_log = aspectlib.debug.log( + print_to=aspects_file, attributes=['id'], stacktrace=30, use_logging=False + ) public_methods = re.compile(r'^(__init__|[a-zA-Z].*)$') - for aspect in aspects: + for aspect in aspects.split(':'): aspectlib.weave(aspect, aspect_log, methods=public_methods) diff --git a/coverage/env.py b/coverage/env.py index 4cd02c0..4699a1e 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -4,6 +4,7 @@ """Determine facts about the environment.""" import os +import platform import sys # Operating systems. @@ -11,7 +12,12 @@ WINDOWS = sys.platform == "win32" LINUX = sys.platform == "linux2" # Python implementations. -PYPY = '__pypy__' in sys.builtin_module_names +PYPY = (platform.python_implementation() == 'PyPy') +if PYPY: + PYPYVERSION = sys.pypy_version_info + +JYTHON = (platform.python_implementation() == 'Jython') +IRONPYTHON = (platform.python_implementation() == 'IronPython') # Python versions. PYVERSION = sys.version_info diff --git a/coverage/execfile.py b/coverage/execfile.py index 3e20a52..693f54f 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -10,7 +10,7 @@ import types from coverage.backward import BUILTINS from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec -from coverage.misc import ExceptionDuringRun, NoCode, NoSource, isolate_module +from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module from coverage.phystokens import compile_unicode from coverage.python import get_python_source @@ -166,11 +166,17 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): sys.path[0] = path0 if path0 is not None else my_path0 try: - # Make a code object somehow. - if filename.endswith((".pyc", ".pyo")): - code = make_code_from_pyc(filename) - else: - code = make_code_from_py(filename) + try: + # Make a code object somehow. + if filename.endswith((".pyc", ".pyo")): + code = make_code_from_pyc(filename) + else: + code = make_code_from_py(filename) + except CoverageException: + raise + except Exception as exc: + msg = "Couldn't run {filename!r} as Python code: {exc.__class__.__name__}: {exc}" + raise CoverageException(msg.format(filename=filename, exc=exc)) # Execute the code object. try: @@ -179,7 +185,7 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): # The user called sys.exit(). Just pass it along to the upper # layers, where it will be handled. raise - except: + except Exception: # Something went wrong while executing the user code. # Get the exc_info, and pack them into an exception that we can # throw up to the outer loop. We peel one layer off the traceback @@ -193,7 +199,27 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): # it somehow? https://bitbucket.org/pypy/pypy/issue/1903 getattr(err, '__context__', None) - raise ExceptionDuringRun(typ, err, tb.tb_next) + # Call the excepthook. + try: + if hasattr(err, "__traceback__"): + err.__traceback__ = err.__traceback__.tb_next + sys.excepthook(typ, err, tb.tb_next) + except SystemExit: + raise + except Exception: + # Getting the output right in the case of excepthook + # shenanigans is kind of involved. + sys.stderr.write("Error in sys.excepthook:\n") + typ2, err2, tb2 = sys.exc_info() + err2.__suppress_context__ = True + if hasattr(err2, "__traceback__"): + err2.__traceback__ = err2.__traceback__.tb_next + sys.__excepthook__(typ2, err2, tb2.tb_next) + sys.stderr.write("\nOriginal exception was:\n") + raise ExceptionDuringRun(typ, err, tb.tb_next) + else: + sys.exit(1) + finally: # Restore the old __main__, argv, and path. sys.modules['__main__'] = old_main_mod diff --git a/coverage/files.py b/coverage/files.py index 9de4849..d2c2b89 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -63,7 +63,11 @@ def canonical_filename(filename): if path is None: continue f = os.path.join(path, filename) - if os.path.exists(f): + try: + exists = os.path.exists(f) + except UnicodeError: + exists = False + if exists: filename = f break cf = abs_file(filename) @@ -147,7 +151,11 @@ else: def abs_file(filename): """Return the absolute normalized form of `filename`.""" path = os.path.expandvars(os.path.expanduser(filename)) - path = os.path.abspath(os.path.realpath(path)) + try: + path = os.path.realpath(path) + except UnicodeError: + pass + path = os.path.abspath(path) path = actual_path(path) path = unicode_filename(path) return path @@ -183,25 +191,31 @@ def prep_patterns(patterns): class TreeMatcher(object): - """A matcher for files in a tree.""" - def __init__(self, directories): - self.dirs = list(directories) + """A matcher for files in a tree. + + Construct with a list of paths, either files or directories. Paths match + with the `match` method if they are one of the files, or if they are + somewhere in a subtree rooted at one of the directories. + + """ + def __init__(self, paths): + self.paths = list(paths) def __repr__(self): - return "<TreeMatcher %r>" % self.dirs + return "<TreeMatcher %r>" % self.paths def info(self): """A list of strings for displaying when dumping state.""" - return self.dirs + return self.paths def match(self, fpath): """Does `fpath` indicate a file in one of our trees?""" - for d in self.dirs: - if fpath.startswith(d): - if fpath == d: + for p in self.paths: + if fpath.startswith(p): + if fpath == p: # This is the same file! return True - if fpath[len(d)] == os.sep: + if fpath[len(p)] == os.sep: # This is a file in the directory return True return False diff --git a/coverage/html.py b/coverage/html.py index f04339d..b0c6164 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -12,7 +12,7 @@ import coverage from coverage import env from coverage.backward import iitems from coverage.files import flat_rootname -from coverage.misc import CoverageException, Hasher, isolate_module +from coverage.misc import CoverageException, file_be_gone, Hasher, isolate_module from coverage.report import Reporter from coverage.results import Numbers from coverage.templite import Templite @@ -105,6 +105,7 @@ class HtmlReporter(Reporter): self.coverage = cov self.files = [] + self.all_files_nums = [] self.has_arcs = self.coverage.data.has_arcs() self.status = HtmlStatus() self.extra_css = None @@ -137,7 +138,7 @@ class HtmlReporter(Reporter): # Process all the files. self.report_files(self.html_file, morfs, self.config.html_dir) - if not self.files: + if not self.all_files_nums: raise CoverageException("No data to report.") # Write the index file. @@ -171,10 +172,26 @@ class HtmlReporter(Reporter): def html_file(self, fr, analysis): """Generate an HTML file for one source file.""" + rootname = flat_rootname(fr.relative_filename()) + html_filename = rootname + ".html" + html_path = os.path.join(self.directory, html_filename) + + # Get the numbers for this file. + nums = analysis.numbers + self.all_files_nums.append(nums) + + if self.config.skip_covered: + # Don't report on 100% files. + no_missing_lines = (nums.n_missing == 0) + no_missing_branches = (nums.n_partial_branches == 0) + if no_missing_lines and no_missing_branches: + # If there's an existing file, remove it. + file_be_gone(html_path) + return + source = fr.source() # Find out if the file on disk is already correct. - rootname = flat_rootname(fr.relative_filename()) this_hash = self.file_hash(source.encode('utf-8'), fr) that_hash = self.status.file_hash(rootname) if this_hash == that_hash: @@ -184,9 +201,6 @@ class HtmlReporter(Reporter): self.status.set_file_hash(rootname, this_hash) - # Get the numbers for this file. - nums = analysis.numbers - if self.has_arcs: missing_branch_arcs = analysis.missing_branch_arcs() arcs_executed = analysis.arcs_executed() @@ -269,8 +283,6 @@ class HtmlReporter(Reporter): 'time_stamp': self.time_stamp, }) - html_filename = rootname + ".html" - html_path = os.path.join(self.directory, html_filename) write_html(html_path, html) # Save this file's information for the index file. @@ -286,7 +298,7 @@ class HtmlReporter(Reporter): """Write the index.html file for this report.""" index_tmpl = Templite(read_data("index.html"), self.template_globals) - self.totals = sum(f['nums'] for f in self.files) + self.totals = sum(self.all_files_nums) html = index_tmpl.render({ 'has_arcs': self.has_arcs, @@ -384,7 +396,7 @@ class HtmlStatus(object): 'files': files, } with open(status_file, "w") as fout: - json.dump(status, fout) + json.dump(status, fout, separators=(',', ':')) # Older versions of ShiningPanda look for the old name, status.dat. # Accommodate them if we are running under Jenkins. diff --git a/coverage/misc.py b/coverage/misc.py index f376346..28aa3b0 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -12,7 +12,7 @@ import sys import types from coverage import env -from coverage.backward import string_class, to_bytes, unicode_class +from coverage.backward import to_bytes, unicode_class ISOLATED_MODULES = {} @@ -38,6 +38,13 @@ def isolate_module(mod): os = isolate_module(os) +def dummy_decorator_with_args(*args_unused, **kwargs_unused): + """Dummy no-op implementation of a decorator with arguments.""" + def _decorator(func): + return func + return _decorator + + # Use PyContracts for assertion testing on parameters and returns, but only if # we are running our own test suite. if env.TESTING: @@ -57,12 +64,22 @@ if env.TESTING: new_contract('bytes', lambda v: isinstance(v, bytes)) if env.PY3: new_contract('unicode', lambda v: isinstance(v, unicode_class)) -else: # pragma: not covered - # We aren't using real PyContracts, so just define a no-op decorator as a - # stunt double. - def contract(**unused): - """Dummy no-op implementation of `contract`.""" - return lambda func: func + + def one_of(argnames): + """Ensure that only one of the argnames is non-None.""" + def _decorator(func): + argnameset = set(name.strip() for name in argnames.split(",")) + def _wrapped(*args, **kwargs): + vals = [kwargs.get(name) for name in argnameset] + assert sum(val is not None for val in vals) == 1 + return func(*args, **kwargs) + return _wrapped + return _decorator +else: # pragma: not testing + # We aren't using real PyContracts, so just define our decorators as + # stunt-double no-ops. + contract = dummy_decorator_with_args + one_of = dummy_decorator_with_args def new_contract(*args_unused, **kwargs_unused): """Dummy no-op implementation of `new_contract`.""" @@ -93,23 +110,28 @@ def format_lines(statements, lines): For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14". + Both `lines` and `statements` can be any iterable. All of the elements of + `lines` must be in `statements`, and all of the values must be positive + integers. + """ - pairs = [] - i = 0 - j = 0 - start = None statements = sorted(statements) lines = sorted(lines) - while i < len(statements) and j < len(lines): - if statements[i] == lines[j]: - if start is None: - start = lines[j] - end = lines[j] - j += 1 + + pairs = [] + start = None + lidx = 0 + for stmt in statements: + if lidx >= len(lines): + break + if stmt == lines[lidx]: + lidx += 1 + if not start: + start = stmt + end = stmt elif start: pairs.append((start, end)) start = None - i += 1 if start: pairs.append((start, end)) ret = ', '.join(map(nice_pair, pairs)) @@ -129,12 +151,12 @@ def expensive(fn): def _wrapped(self): """Inner function that checks the cache.""" if hasattr(self, attr): - raise Exception("Shouldn't have called %s more than once" % fn.__name__) + raise AssertionError("Shouldn't have called %s more than once" % fn.__name__) setattr(self, attr, True) return fn(self) return _wrapped else: - return fn + return fn # pragma: not testing def bool_or_none(b): @@ -179,8 +201,8 @@ class Hasher(object): def update(self, v): """Add `v` to the hash, recursively if needed.""" self.md5.update(to_bytes(str(type(v)))) - if isinstance(v, string_class): - self.md5.update(to_bytes(v)) + if isinstance(v, unicode_class): + self.md5.update(v.encode('utf8')) elif isinstance(v, bytes): self.md5.update(v) elif v is None: @@ -237,8 +259,13 @@ class SimpleRepr(object): ) -class CoverageException(Exception): - """An exception specific to coverage.py.""" +class BaseCoverageException(Exception): + """The base of all Coverage exceptions.""" + pass + + +class CoverageException(BaseCoverageException): + """A run-of-the-mill exception specific to coverage.py.""" pass @@ -264,3 +291,13 @@ class ExceptionDuringRun(CoverageException): """ pass + + +class StopEverything(BaseCoverageException): + """An exception that means everything should stop. + + The CoverageTest class converts these to SkipTest, so that when running + tests, raising this exception will automatically skip the test. + + """ + pass diff --git a/coverage/parser.py b/coverage/parser.py index c3dba83..590eace 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -15,8 +15,8 @@ from coverage.backward import range # pylint: disable=redefined-builtin from coverage.backward import bytes_to_ints, string_class from coverage.bytecode import CodeObjects from coverage.debug import short_stack -from coverage.misc import contract, new_contract, nice_pair, join_regex -from coverage.misc import CoverageException, NoSource, NotPython +from coverage.misc import contract, join_regex, new_contract, nice_pair, one_of +from coverage.misc import NoSource, NotPython, StopEverything from coverage.phystokens import compile_unicode, generate_tokens, neuter_encoding_declaration @@ -106,7 +106,6 @@ class PythonParser(object): """ combined = join_regex(regexes) if env.PY2: - # pylint: disable=redefined-variable-type combined = combined.decode("utf8") regex_c = re.compile(combined) matches = set() @@ -138,7 +137,7 @@ class PythonParser(object): tokgen = generate_tokens(self.text) for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen: - if self.show_tokens: # pragma: not covered + if self.show_tokens: # pragma: debugging print("%10s %5s %-20r %r" % ( tokenize.tok_name.get(toktype, toktype), nice_pair((slineno, elineno)), ttext, ltext @@ -371,11 +370,11 @@ class ByteParser(object): # Alternative Python implementations don't always provide all the # attributes on code objects that we need to do the analysis. - for attr in ['co_lnotab', 'co_firstlineno', 'co_consts']: + for attr in ['co_lnotab', 'co_firstlineno']: if not hasattr(self.code, attr): - raise CoverageException( + raise StopEverything( # pragma: only jython "This implementation of Python doesn't support code analysis.\n" - "Run coverage.py under CPython for this command." + "Run coverage.py under another Python for this command." ) def child_parsers(self): @@ -433,23 +432,35 @@ class ByteParser(object): class LoopBlock(object): """A block on the block stack representing a `for` or `while` loop.""" + @contract(start=int) def __init__(self, start): + # The line number where the loop starts. self.start = start + # A set of ArcStarts, the arcs from break statements exiting this loop. self.break_exits = set() class FunctionBlock(object): """A block on the block stack representing a function definition.""" + @contract(start=int, name=str) def __init__(self, start, name): + # The line number where the function starts. self.start = start + # The name of the function. self.name = name class TryBlock(object): """A block on the block stack representing a `try` block.""" - def __init__(self, handler_start=None, final_start=None): + @contract(handler_start='int|None', final_start='int|None') + def __init__(self, handler_start, final_start): + # The line number of the first "except" handler, if any. self.handler_start = handler_start + # The line number of the "finally:" clause, if any. self.final_start = final_start + + # The ArcStarts for breaks/continues/returns/raises inside the "try:" + # that need to route through the "finally:" clause. self.break_from = set() self.continue_from = set() self.return_from = set() @@ -459,8 +470,13 @@ class TryBlock(object): class ArcStart(collections.namedtuple("Arc", "lineno, cause")): """The information needed to start an arc. - `lineno` is the line number the arc starts from. `cause` is a fragment - used as the startmsg for AstArcAnalyzer.missing_arc_fragments. + `lineno` is the line number the arc starts from. + + `cause` is an English text fragment used as the `startmsg` for + AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an + arc wasn't executed, so should fit well into a sentence of the form, + "Line 17 didn't run because {cause}." The fragment can include "{lineno}" + to have `lineno` interpolated into it. """ def __new__(cls, lineno, cause=None): @@ -472,6 +488,21 @@ class ArcStart(collections.namedtuple("Arc", "lineno, cause")): new_contract('ArcStarts', lambda seq: all(isinstance(x, ArcStart) for x in seq)) +# Turn on AST dumps with an environment variable. +AST_DUMP = bool(int(os.environ.get("COVERAGE_AST_DUMP", 0))) + +class NodeList(object): + """A synthetic fictitious node, containing a sequence of nodes. + + This is used when collapsing optimized if-statements, to represent the + unconditional execution of one of the clauses. + + """ + def __init__(self, body): + self.body = body + self.lineno = body[0].lineno + + class AstArcAnalyzer(object): """Analyze source text with an AST to find executable code paths.""" @@ -482,15 +513,17 @@ class AstArcAnalyzer(object): self.statements = set(multiline.get(l, l) for l in statements) self.multiline = multiline - if int(os.environ.get("COVERAGE_ASTDUMP", 0)): # pragma: debugging + if AST_DUMP: # pragma: debugging # Dump the AST so that failing tests have helpful output. - print("Statements: {}".format(self.statements)) - print("Multiline map: {}".format(self.multiline)) + print("Statements: {0}".format(self.statements)) + print("Multiline map: {0}".format(self.multiline)) ast_dump(self.root_node) self.arcs = set() - # A map from arc pairs to a pair of sentence fragments: (startmsg, endmsg). + # A map from arc pairs to a list of pairs of sentence fragments: + # { (start, end): [(startmsg, endmsg), ...], } + # # For an arc from line 17, they should be usable like: # "Line 17 {endmsg}, because {startmsg}" self.missing_arc_fragments = collections.defaultdict(list) @@ -513,7 +546,7 @@ class AstArcAnalyzer(object): def add_arc(self, start, end, smsg=None, emsg=None): """Add an arc, including message fragments to use if it is missing.""" - if self.debug: + if self.debug: # pragma: debugging print("\nAdding arc: ({}, {}): {!r}, {!r}".format(start, end, smsg, emsg)) print(short_stack(limit=6)) self.arcs.add((start, end)) @@ -564,9 +597,10 @@ class AstArcAnalyzer(object): if node.body: return self.line_for_node(node.body[0]) else: - # Modules have no line number, they always start at 1. + # Empty modules have no line number, they always start at 1. return 1 + # The node types that just flow to the next node with no complications. OK_TO_DEFAULT = set([ "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", "Import", "ImportFrom", "Nonlocal", "Pass", "Print", @@ -576,20 +610,35 @@ class AstArcAnalyzer(object): def add_arcs(self, node): """Add the arcs for `node`. - Return a set of ArcStarts, exits from this node to the next. + Return a set of ArcStarts, exits from this node to the next. Because a + node represents an entire sub-tree (including its children), the exits + from a node can be arbitrarily complex:: + + if something(1): + if other(2): + doit(3) + else: + doit(5) + + There are two exits from line 1: they start at line 3 and line 5. """ node_name = node.__class__.__name__ handler = getattr(self, "_handle__" + node_name, None) if handler is not None: return handler(node) + else: + # No handler: either it's something that's ok to default (a simple + # statement), or it's something we overlooked. Change this 0 to 1 + # to see if it's overlooked. + if 0: + if node_name not in self.OK_TO_DEFAULT: + print("*** Unhandled: {0}".format(node)) - if 0: - node_name = node.__class__.__name__ - if node_name not in self.OK_TO_DEFAULT: - print("*** Unhandled: {0}".format(node)) - return set([ArcStart(self.line_for_node(node), cause=None)]) + # Default for simple statements: one exit from this node. + return set([ArcStart(self.line_for_node(node))]) + @one_of("from_start, prev_starts") @contract(returns='ArcStarts') def add_body_arcs(self, body, from_start=None, prev_starts=None): """Add arcs for the body of a compound statement. @@ -608,28 +657,91 @@ class AstArcAnalyzer(object): lineno = self.line_for_node(body_node) first_line = self.multiline.get(lineno, lineno) if first_line not in self.statements: - continue + body_node = self.find_non_missing_node(body_node) + if body_node is None: + continue + lineno = self.line_for_node(body_node) for prev_start in prev_starts: self.add_arc(prev_start.lineno, lineno, prev_start.cause) prev_starts = self.add_arcs(body_node) return prev_starts + def find_non_missing_node(self, node): + """Search `node` looking for a child that has not been optimized away. + + This might return the node you started with, or it will work recursively + to find a child node in self.statements. + + Returns a node, or None if none of the node remains. + + """ + # This repeats work just done in add_body_arcs, but this duplication + # means we can avoid a function call in the 99.9999% case of not + # optimizing away statements. + lineno = self.line_for_node(node) + first_line = self.multiline.get(lineno, lineno) + if first_line in self.statements: + return node + + missing_fn = getattr(self, "_missing__" + node.__class__.__name__, None) + if missing_fn: + node = missing_fn(node) + else: + node = None + return node + + def _missing__If(self, node): + # If the if-node is missing, then one of its children might still be + # here, but not both. So return the first of the two that isn't missing. + # Use a NodeList to hold the clauses as a single node. + non_missing = self.find_non_missing_node(NodeList(node.body)) + if non_missing: + return non_missing + if node.orelse: + return self.find_non_missing_node(NodeList(node.orelse)) + return None + + def _missing__NodeList(self, node): + # A NodeList might be a mixture of missing and present nodes. Find the + # ones that are present. + non_missing_children = [] + for child in node.body: + child = self.find_non_missing_node(child) + if child is not None: + non_missing_children.append(child) + + # Return the simplest representation of the present children. + if not non_missing_children: + return None + if len(non_missing_children) == 1: + return non_missing_children[0] + return NodeList(non_missing_children) + def is_constant_expr(self, node): """Is this a compile-time constant?""" node_name = node.__class__.__name__ if node_name in ["NameConstant", "Num"]: - return True + return "Num" elif node_name == "Name": - if env.PY3 and node.id in ["True", "False", "None"]: - return True - return False - - # tests to write: - # TODO: while EXPR: - # TODO: while False: - # TODO: listcomps hidden deep in other expressions - # TODO: listcomps hidden in lists: x = [[i for i in range(10)]] - # TODO: nested function definitions + if node.id in ["True", "False", "None", "__debug__"]: + return "Name" + return None + + # In the fullness of time, these might be good tests to write: + # while EXPR: + # while False: + # listcomps hidden deep in other expressions + # listcomps hidden in lists: x = [[i for i in range(10)]] + # nested function definitions + + + # Exit processing: process_*_exits + # + # These functions process the four kinds of jump exits: break, continue, + # raise, and return. To figure out where an exit goes, we have to look at + # the block stack context. For example, a break will jump to the nearest + # enclosing loop block, or the nearest enclosing finally block, whichever + # is nearer. @contract(exits='ArcStarts') def process_break_exits(self, exits): @@ -689,7 +801,14 @@ class AstArcAnalyzer(object): ) break - ## Handlers + + # Handlers: _handle__* + # + # Each handler deals with a specific AST node type, dispatched from + # add_arcs. Each deals with a particular kind of node type, and returns + # the set of exits from that node. These functions mirror the Python + # semantics of each syntactic construct. See the docstring for add_arcs to + # understand the concept of exits from a node. @contract(returns='ArcStarts') def _handle__Break(self, node): @@ -719,7 +838,7 @@ class AstArcAnalyzer(object): self.add_arc(last, lineno) last = lineno # The body is handled in collect_arcs. - return set([ArcStart(last, cause=None)]) + return set([ArcStart(last)]) _handle__ClassDef = _handle_decorated @@ -746,7 +865,7 @@ class AstArcAnalyzer(object): else_exits = self.add_body_arcs(node.orelse, from_start=from_start) exits |= else_exits else: - # no else clause: exit from the for line. + # No else clause: exit from the for line. exits.add(from_start) return exits @@ -765,6 +884,12 @@ class AstArcAnalyzer(object): return exits @contract(returns='ArcStarts') + def _handle__NodeList(self, node): + start = self.line_for_node(node) + exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) + return exits + + @contract(returns='ArcStarts') def _handle__Raise(self, node): here = self.line_for_node(node) raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed") @@ -792,11 +917,11 @@ class AstArcAnalyzer(object): else: final_start = None - try_block = TryBlock(handler_start=handler_start, final_start=final_start) + try_block = TryBlock(handler_start, final_start) self.block_stack.append(try_block) start = self.line_for_node(node) - exits = self.add_body_arcs(node.body, from_start=ArcStart(start, cause=None)) + exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) # We're done with the `try` body, so this block no longer handles # exceptions. We keep the block so the `finally` clause can pick up @@ -839,30 +964,46 @@ class AstArcAnalyzer(object): try_block.return_from # or a `return`. ) - exits = self.add_body_arcs(node.finalbody, prev_starts=final_from) + final_exits = self.add_body_arcs(node.finalbody, prev_starts=final_from) + if try_block.break_from: - break_exits = self._combine_finally_starts(try_block.break_from, exits) - self.process_break_exits(break_exits) + self.process_break_exits( + self._combine_finally_starts(try_block.break_from, final_exits) + ) if try_block.continue_from: - continue_exits = self._combine_finally_starts(try_block.continue_from, exits) - self.process_continue_exits(continue_exits) + self.process_continue_exits( + self._combine_finally_starts(try_block.continue_from, final_exits) + ) if try_block.raise_from: - raise_exits = self._combine_finally_starts(try_block.raise_from, exits) - self.process_raise_exits(raise_exits) + self.process_raise_exits( + self._combine_finally_starts(try_block.raise_from, final_exits) + ) if try_block.return_from: - return_exits = self._combine_finally_starts(try_block.return_from, exits) - self.process_return_exits(return_exits) + self.process_return_exits( + self._combine_finally_starts(try_block.return_from, final_exits) + ) + + if exits: + # The finally clause's exits are only exits for the try block + # as a whole if the try block had some exits to begin with. + exits = final_exits return exits + @contract(starts='ArcStarts', exits='ArcStarts', returns='ArcStarts') def _combine_finally_starts(self, starts, exits): - """Helper for building the cause of `finally` branches.""" + """Helper for building the cause of `finally` branches. + + "finally" clauses might not execute their exits, and the causes could + be due to a failure to execute any of the exits in the try block. So + we use the causes from `starts` as the causes for `exits`. + """ causes = [] - for lineno, cause in sorted(starts): - if cause is not None: - causes.append(cause.format(lineno=lineno)) + for start in sorted(starts): + if start.cause is not None: + causes.append(start.cause.format(lineno=start.lineno)) cause = " or ".join(causes) - exits = set(ArcStart(ex.lineno, cause) for ex in exits) + exits = set(ArcStart(xit.lineno, cause) for xit in exits) return exits @contract(returns='ArcStarts') @@ -894,9 +1035,9 @@ class AstArcAnalyzer(object): def _handle__While(self, node): constant_test = self.is_constant_expr(node.test) start = to_top = self.line_for_node(node.test) - if constant_test: + if constant_test and (env.PY3 or constant_test == "Num"): to_top = self.line_for_node(node.body[0]) - self.block_stack.append(LoopBlock(start=start)) + self.block_stack.append(LoopBlock(start=to_top)) from_start = ArcStart(start, cause="the condition on line {lineno} was never true") exits = self.add_body_arcs(node.body, from_start=from_start) for xit in exits: @@ -971,62 +1112,64 @@ class AstArcAnalyzer(object): _code_object__ListComp = _make_oneline_code_method("list comprehension") -SKIP_DUMP_FIELDS = ["ctx"] +if AST_DUMP: # pragma: debugging + # Code only used when dumping the AST for debugging. -def _is_simple_value(value): - """Is `value` simple enough to be displayed on a single line?""" - return ( - value in [None, [], (), {}, set()] or - isinstance(value, (string_class, int, float)) - ) + SKIP_DUMP_FIELDS = ["ctx"] -# TODO: a test of ast_dump? -def ast_dump(node, depth=0): - """Dump the AST for `node`. + def _is_simple_value(value): + """Is `value` simple enough to be displayed on a single line?""" + return ( + value in [None, [], (), {}, set()] or + isinstance(value, (string_class, int, float)) + ) - This recursively walks the AST, printing a readable version. + def ast_dump(node, depth=0): + """Dump the AST for `node`. - """ - indent = " " * depth - if not isinstance(node, ast.AST): - print("{0}<{1} {2!r}>".format(indent, node.__class__.__name__, node)) - return - - lineno = getattr(node, "lineno", None) - if lineno is not None: - linemark = " @ {0}".format(node.lineno) - else: - linemark = "" - head = "{0}<{1}{2}".format(indent, node.__class__.__name__, linemark) - - named_fields = [ - (name, value) - for name, value in ast.iter_fields(node) - if name not in SKIP_DUMP_FIELDS - ] - if not named_fields: - print("{0}>".format(head)) - elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]): - field_name, value = named_fields[0] - print("{0} {1}: {2!r}>".format(head, field_name, value)) - else: - print(head) - if 0: - print("{0}# mro: {1}".format( - indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]), - )) - next_indent = indent + " " - for field_name, value in named_fields: - prefix = "{0}{1}:".format(next_indent, field_name) - if _is_simple_value(value): - print("{0} {1!r}".format(prefix, value)) - elif isinstance(value, list): - print("{0} [".format(prefix)) - for n in value: - ast_dump(n, depth + 8) - print("{0}]".format(next_indent)) - else: - print(prefix) - ast_dump(value, depth + 8) + This recursively walks the AST, printing a readable version. + + """ + indent = " " * depth + if not isinstance(node, ast.AST): + print("{0}<{1} {2!r}>".format(indent, node.__class__.__name__, node)) + return + + lineno = getattr(node, "lineno", None) + if lineno is not None: + linemark = " @ {0}".format(node.lineno) + else: + linemark = "" + head = "{0}<{1}{2}".format(indent, node.__class__.__name__, linemark) + + named_fields = [ + (name, value) + for name, value in ast.iter_fields(node) + if name not in SKIP_DUMP_FIELDS + ] + if not named_fields: + print("{0}>".format(head)) + elif len(named_fields) == 1 and _is_simple_value(named_fields[0][1]): + field_name, value = named_fields[0] + print("{0} {1}: {2!r}>".format(head, field_name, value)) + else: + print(head) + if 0: + print("{0}# mro: {1}".format( + indent, ", ".join(c.__name__ for c in node.__class__.__mro__[1:]), + )) + next_indent = indent + " " + for field_name, value in named_fields: + prefix = "{0}{1}:".format(next_indent, field_name) + if _is_simple_value(value): + print("{0} {1!r}".format(prefix, value)) + elif isinstance(value, list): + print("{0} [".format(prefix)) + for n in value: + ast_dump(n, depth + 8) + print("{0}]".format(next_indent)) + else: + print(prefix) + ast_dump(value, depth + 8) - print("{0}>".format(indent)) + print("{0}>".format(indent)) diff --git a/coverage/phystokens.py b/coverage/phystokens.py index 5e80ed5..a2b23cf 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -11,7 +11,7 @@ import token import tokenize from coverage import env -from coverage.backward import iternext +from coverage.backward import iternext, unicode_class from coverage.misc import contract @@ -281,7 +281,7 @@ def compile_unicode(source, filename, mode): """ source = neuter_encoding_declaration(source) - if env.PY2 and isinstance(filename, unicode): + if env.PY2 and isinstance(filename, unicode_class): filename = filename.encode(sys.getfilesystemencoding(), "replace") code = compile(source, filename, mode) return code @@ -290,5 +290,9 @@ def compile_unicode(source, filename, mode): @contract(source='unicode', returns='unicode') def neuter_encoding_declaration(source): """Return `source`, with any encoding declaration neutered.""" - source = COOKIE_RE.sub("# (deleted declaration)", source, count=2) + if COOKIE_RE.search(source): + source_lines = source.splitlines(True) + for lineno in range(min(2, len(source_lines))): + source_lines[lineno] = COOKIE_RE.sub("# (deleted declaration)", source_lines[lineno]) + source = "".join(source_lines) return source diff --git a/coverage/plugin.py b/coverage/plugin.py index fc95eee..3e0e483 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -89,6 +89,19 @@ class CoveragePlugin(object): """ _needs_to_implement(self, "file_reporter") + def find_executable_files(self, src_dir): # pylint: disable=unused-argument + """Yield all of the executable files in `src_dir`, recursively. + + Executability is a plugin-specific property, but generally means files + which would have been considered for coverage analysis, had they been + included automatically. + + Returns or yields a sequence of strings, the paths to files that could + have been executed, including files that had been executed. + + """ + return [] + def sys_info(self): """Get a list of information useful for debugging. diff --git a/coverage/python.py b/coverage/python.py index 7109ece..dacdf61 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -26,7 +26,13 @@ def read_python_source(filename): """ with open(filename, "rb") as f: - return f.read().replace(b"\r\n", b"\n").replace(b"\r", b"\n") + source = f.read() + + if env.IRONPYTHON: + # IronPython reads Unicode strings even for "rb" files. + source = bytes(source) + + return source.replace(b"\r\n", b"\n").replace(b"\r", b"\n") @contract(returns='unicode') @@ -75,7 +81,7 @@ def get_zip_bytes(filename): an empty string if the file is empty. """ - markers = ['.zip'+os.sep, '.egg'+os.sep] + markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep] for marker in markers: if marker in filename: parts = filename.split(marker) @@ -91,6 +97,39 @@ def get_zip_bytes(filename): return None +def source_for_file(filename): + """Return the source file for `filename`. + + Given a file name being traced, return the best guess as to the source + file to attribute it to. + + """ + if filename.endswith(".py"): + # .py files are themselves source files. + return filename + + elif filename.endswith((".pyc", ".pyo")): + # Bytecode files probably have source files near them. + py_filename = filename[:-1] + if os.path.exists(py_filename): + # Found a .py file, use that. + return py_filename + if env.WINDOWS: + # On Windows, it could be a .pyw file. + pyw_filename = py_filename + "w" + if os.path.exists(pyw_filename): + return pyw_filename + # Didn't find source, but it's probably the .py file we want. + return py_filename + + elif filename.endswith("$py.class"): + # Jython is easy to guess. + return filename[:-9] + ".py" + + # No idea, just use the file name as-is. + return filename + + class PythonFileReporter(FileReporter): """Report support for a Python file.""" @@ -106,13 +145,7 @@ class PythonFileReporter(FileReporter): else: filename = morf - filename = files.unicode_filename(filename) - - # .pyc files should always refer to a .py instead. - if filename.endswith(('.pyc', '.pyo')): - filename = filename[:-1] - elif filename.endswith('$py.class'): # Jython - filename = filename[:-9] + ".py" + filename = source_for_file(files.unicode_filename(filename)) super(PythonFileReporter, self).__init__(files.canonical_filename(filename)) diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 23f4946..b41f405 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -3,6 +3,7 @@ """Raw data collector for coverage.py.""" +import atexit import dis import sys @@ -44,16 +45,21 @@ class PyTracer(object): self.threading = None self.cur_file_dict = [] - self.last_line = [0] + self.last_line = 0 # int, but uninitialized. self.data_stack = [] self.last_exc_back = None self.last_exc_firstlineno = 0 self.thread = None self.stopped = False + self._activity = False + + self.in_atexit = False + # On exit, self.in_atexit = True + atexit.register(setattr, self, 'in_atexit', True) def __repr__(self): - return "<PyTracer at 0x{0:0x}: {1} lines in {2} files>".format( + return "<PyTracer at {0}: {1} lines in {2} files>".format( id(self), sum(len(v) for v in self.data.values()), len(self.data), @@ -77,6 +83,7 @@ class PyTracer(object): if event == 'call': # Entering a new function context. Decide if we should trace # in this file. + self._activity = True self.data_stack.append((self.cur_file_dict, self.last_line)) filename = frame.f_code.co_filename disp = self.should_trace_cache.get(filename) @@ -94,7 +101,7 @@ class PyTracer(object): # function calls and re-entering generators. The f_lasti field is # -1 for calls, and a real offset for generators. Use <0 as the # line number for calls, and the real line number for generators. - if frame.f_lasti < 0: + if getattr(frame, 'f_lasti', -1) < 0: self.last_line = -frame.f_code.co_firstlineno else: self.last_line = frame.f_lineno @@ -111,8 +118,9 @@ class PyTracer(object): if self.trace_arcs and self.cur_file_dict: # Record an arc leaving the function, but beware that a # "return" event might just mean yielding from a generator. - bytecode = frame.f_code.co_code[frame.f_lasti] - if bytecode != YIELD_VALUE: + # Jython seems to have an empty co_code, so just assume return. + code = frame.f_code.co_code + if (not code) or code[frame.f_lasti] != YIELD_VALUE: first = frame.f_code.co_firstlineno self.cur_file_dict[(self.last_line, -first)] = None # Leaving this function, pop the filename stack. @@ -128,10 +136,18 @@ class PyTracer(object): Return a Python function suitable for use with sys.settrace(). """ + self.stopped = False if self.threading: - self.thread = self.threading.currentThread() + if self.thread is None: + self.thread = self.threading.currentThread() + else: + if self.thread.ident != self.threading.currentThread().ident: + # Re-starting from a different thread!? Don't set the trace + # function, but we are marked as running again, so maybe it + # will be ok? + return self._trace + sys.settrace(self._trace) - self.stopped = False return self._trace def stop(self): @@ -144,12 +160,27 @@ class PyTracer(object): return if self.warn: - if sys.gettrace() != self._trace: - msg = "Trace function changed, measurement is likely wrong: %r" - self.warn(msg % (sys.gettrace(),)) + # PyPy clears the trace function before running atexit functions, + # so don't warn if we are in atexit on PyPy and the trace function + # has changed to None. + tf = sys.gettrace() + dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) + if (not dont_warn) and tf != self._trace: + self.warn( + "Trace function changed, measurement is likely wrong: %r" % (tf,), + slug="trace-changed", + ) sys.settrace(None) + def activity(self): + """Has there been any activity?""" + return self._activity + + def reset_activity(self): + """Reset the activity() flag.""" + self._activity = False + def get_stats(self): """Return a dictionary of statistics, or None.""" return None diff --git a/coverage/results.py b/coverage/results.py index 9df5d5b..81ce2a6 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -269,3 +269,27 @@ class Numbers(SimpleRepr): if other == 0: return self return NotImplemented + + +def should_fail_under(total, fail_under): + """Determine if a total should fail due to fail-under. + + `total` is a float, the coverage measurement total. `fail_under` is the + fail_under setting to compare with. + + Returns True if the total should fail. + + """ + # The fail_under option defaults to 0. + if fail_under: + # Total needs to be rounded, but don't want to report 100 + # unless it is really 100. + if 99 < total < 100: + total = 99 + else: + total = round(total) + + if total < fail_under: + return True + + return False diff --git a/coverage/summary.py b/coverage/summary.py index b0fa71a..271b648 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -8,7 +8,7 @@ import sys from coverage import env from coverage.report import Reporter from coverage.results import Numbers -from coverage.misc import NotPython, CoverageException, output_encoding +from coverage.misc import NotPython, CoverageException, output_encoding, StopEverything class SummaryReporter(Reporter): @@ -25,12 +25,53 @@ class SummaryReporter(Reporter): for native strings (bytes on Python 2, Unicode on Python 3). """ - file_reporters = self.find_file_reporters(morfs) + if outfile is None: + outfile = sys.stdout + + def writeout(line): + """Write a line to the output, adding a newline.""" + if env.PY2: + line = line.encode(output_encoding()) + outfile.write(line.rstrip()) + outfile.write("\n") + + fr_analysis = [] + skipped_count = 0 + total = Numbers() + + fmt_err = u"%s %s: %s" + + for fr in self.find_file_reporters(morfs): + try: + analysis = self.coverage._analyze(fr) + nums = analysis.numbers + total += nums + + if self.config.skip_covered: + # Don't report on 100% files. + no_missing_lines = (nums.n_missing == 0) + no_missing_branches = (nums.n_partial_branches == 0) + if no_missing_lines and no_missing_branches: + skipped_count += 1 + continue + fr_analysis.append((fr, analysis)) + except StopEverything: + # Don't report this on single files, it's a systemic problem. + raise + except Exception: + report_it = not self.config.ignore_errors + if report_it: + typ, msg = sys.exc_info()[:2] + # NotPython is only raised by PythonFileReporter, which has a + # should_be_python() method. + if issubclass(typ, NotPython) and not fr.should_be_python(): + report_it = False + if report_it: + writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg)) # Prepare the formatting strings, header, and column sorting. - max_name = max([len(fr.relative_filename()) for fr in file_reporters] + [5]) + max_name = max([len(fr.relative_filename()) for (fr, analysis) in fr_analysis] + [5]) fmt_name = u"%%- %ds " % max_name - fmt_err = u"%s %s: %s" fmt_skip_covered = u"\n%s file%s skipped due to complete coverage." header = (fmt_name % "Name") + u" Stmts Miss" @@ -50,16 +91,6 @@ class SummaryReporter(Reporter): if self.branches: column_order.update(dict(branch=3, brpart=4)) - if outfile is None: - outfile = sys.stdout - - def writeout(line): - """Write a line to the output, adding a newline.""" - if env.PY2: - line = line.encode(output_encoding()) - outfile.write(line.rstrip()) - outfile.write("\n") - # Write the header writeout(header) writeout(rule) @@ -69,22 +100,9 @@ class SummaryReporter(Reporter): # sortable values. lines = [] - total = Numbers() - skipped_count = 0 - - for fr in file_reporters: + for (fr, analysis) in fr_analysis: try: - analysis = self.coverage._analyze(fr) nums = analysis.numbers - total += nums - - if self.config.skip_covered: - # Don't report on 100% files. - no_missing_lines = (nums.n_missing == 0) - no_missing_branches = (nums.n_partial_branches == 0) - if no_missing_lines and no_missing_branches: - skipped_count += 1 - continue args = (fr.relative_filename(), nums.n_statements, nums.n_missing) if self.branches: diff --git a/coverage/version.py b/coverage/version.py index 35dc1ec..92a3bcb 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (4, 3, 0, 'alpha', 0) +version_info = (4, 4, 0, 'beta', 2) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 694415f..b5a33dd 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -18,11 +18,7 @@ from coverage.report import Reporter os = isolate_module(os) -DTD_URL = ( - 'https://raw.githubusercontent.com/cobertura/web/' - 'f0366e5e2cf18f111cbd61fc34ef720a6584ba02' - '/htdocs/xml/coverage-03.dtd' -) +DTD_URL = 'https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd' def rate(hit, num): @@ -114,12 +110,18 @@ class XmlReporter(Reporter): bnum_tot += bnum bhits_tot += bhits + xcoverage.setAttribute("lines-valid", str(lnum_tot)) + xcoverage.setAttribute("lines-covered", str(lhits_tot)) xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot)) if self.has_arcs: - branch_rate = rate(bhits_tot, bnum_tot) + xcoverage.setAttribute("branches-valid", str(bnum_tot)) + xcoverage.setAttribute("branches-covered", str(bhits_tot)) + xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot)) else: - branch_rate = "0" - xcoverage.setAttribute("branch-rate", branch_rate) + xcoverage.setAttribute("branches-covered", "0") + xcoverage.setAttribute("branches-valid", "0") + xcoverage.setAttribute("branch-rate", "0") + xcoverage.setAttribute("complexity", "0") # Use the DOM to write the output file. out = self.xml_out.toprettyxml() @@ -148,7 +150,7 @@ class XmlReporter(Reporter): else: rel_name = fr.relative_filename() - dirname = os.path.dirname(rel_name) or "." + dirname = os.path.dirname(rel_name) or u"." dirname = "/".join(dirname.split("/")[:self.config.xml_package_depth]) package_name = dirname.replace("/", ".") diff --git a/doc/changes.rst b/doc/changes.rst index c75b2ba..0243b5c 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -3,1209 +3,6 @@ .. _changes: -==================================== -Major change history for coverage.py -==================================== - -.. :history: 20090524T134300, brand new docs. -.. :history: 20090613T164000, final touches for 3.0 -.. :history: 20090706T205000, changes for 3.0.1 -.. :history: 20091004T170700, changes for 3.1 -.. :history: 20091128T072200, changes for 3.2 -.. :history: 20091205T161525, 3.2 final -.. :history: 20100221T151900, changes for 3.3 -.. :history: 20100306T181400, changes for 3.3.1 -.. :history: 20100725T211700, updated for 3.4. -.. :history: 20100820T151500, updated for 3.4b1 -.. :history: 20100906T133800, updated for 3.4b2 -.. :history: 20100919T163400, updated for 3.4 release. -.. :history: 20110604T214100, updated for 3.5b1 -.. :history: 20110629T082200, updated for 3.5 -.. :history: 20110923T081600, updated for 3.5.1 -.. :history: 20120429T162100, updated for 3.5.2b1 -.. :history: 20120503T233700, updated for 3.5.2 -.. :history: 20120929T093100, updated for 3.5.3 -.. :history: 20121129T060100, updated for 3.6b1. -.. :history: 20121223T180600, updated for 3.6b2. -.. :history: 20130105T173500, updated for 3.6 -.. :history: 20131005T205700, updated for 3.7 -.. :history: 20131212T213100, updated for 3.7.1 -.. :history: 20150124T134800, updated for 4.0a4 -.. :history: 20150802T174700, updated for 4.0b1 -.. :history: 20150822T092800, updated for 4.0b2 -.. :history: 20150919T072700, updated for 4.0 -.. :history: 20151013T103000, updated for 4.0.1 -.. :history: 20151104T050900, updated for 4.0.2 -.. :history: 20151124T065800, updated for 4.0.3 -.. :history: 20160110T125800, updated for 4.1b1 -.. :history: 20160510T125200, updated for 4.1b3 -.. :history: 20160521T074300, updated for 4.1 - - -These are the major changes for coverage.py. For a more complete change -history, see the `CHANGES.rst`_ file in the source tree. - -.. _CHANGES.rst: http://bitbucket.org/ned/coveragepy/src/tip/CHANGES.rst - .. module:: coverage -.. _changes_42: - -Version 4.2 --- 2016-07-26 --------------------------- - -Work from the PyCon 2016 Sprints! - -- BACKWARD INCOMPATIBILITY: the ``coverage combine`` command now ignores an - existing ``.coverage`` data file. It used to include that file in its - combining. This caused confusing results, and extra tox "clean" steps. If - you want the old behavior, use the new ``coverage combine --append`` option. - -- Since ``concurrency=multiprocessing`` uses subprocesses, options specified on - the coverage.py command line will not be communicated down to them. Only - options in the configuration file will apply to the subprocesses. - Previously, the options didn't apply to the subprocesses, but there was no - indication. Now it is an error to use ``--concurrency=multiprocessing`` and - other run-affecting options on the command line. This prevents - failures like those reported in `issue 495`_. - -- The ``concurrency`` option can now take multiple values, to support programs - using multiprocessing and another library such as eventlet. This is only - possible in the configuration file, not from the command line. The - configuration file is the only way for sub-processes to all run with the same - options. Fixes `issue 484`_. Thanks to Josh Williams for prototyping. - -- Using a ``concurrency`` setting of ``multiprocessing`` now implies - ``--parallel`` so that the main program is measured similarly to the - sub-processes. - -- When using `automatic subprocess measurement`_, running coverage commands - would create spurious data files. This is now fixed, thanks to diagnosis and - testing by Dan Riti. Closes `issue 492`_. - -- A new configuration option, ``report:sort``, controls what column of the - text report is used to sort the rows. Thanks to Dan Wandschneider, this - closes `issue 199`_. - -- The HTML report has a more-visible indicator for which column is being - sorted. Closes `issue 298`_, thanks to Josh Williams. - -- Filtering the HTML report is now faster, thanks to Ville Skyttä. - -- If the HTML report cannot find the source for a file, the message now - suggests using the ``-i`` flag to allow the report to continue. Closes - `issue 231`_, thanks, Nathan Land. - -- When reports are ignoring errors, there's now a warning if a file cannot be - parsed, rather than being silently ignored. Closes `issue 396`_. Thanks, - Matthew Boehm. - -- A new option for ``coverage debug`` is available: ``coverage debug config`` - shows the current configuration. Closes `issue 454`_, thanks to Matthew - Boehm. - -- Running coverage as a module (``python -m coverage``) no longer shows the - program name as ``__main__.py``. Fixes `issue 478`_. Thanks, Scott Belden. - -- The `test_helpers` module has been moved into a separate pip-installable - package: `unittest-mixins`_. - -.. _automatic subprocess measurement: http://coverage.readthedocs.io/en/latest/subprocess.html -.. _issue 199: https://bitbucket.org/ned/coveragepy/issues/199/add-a-way-to-sort-the-text-report -.. _issue 231: https://bitbucket.org/ned/coveragepy/issues/231/various-default-behavior-in-report-phase -.. _issue 298: https://bitbucket.org/ned/coveragepy/issues/298/show-in-html-report-that-the-columns-are -.. _issue 396: https://bitbucket.org/ned/coveragepy/issues/396/coverage-xml-shouldnt-bail-out-on-parse -.. _issue 454: https://bitbucket.org/ned/coveragepy/issues/454/coverage-debug-config-should-be -.. _issue 478: https://bitbucket.org/ned/coveragepy/issues/478/help-shows-silly-program-name-when-running -.. _issue 484: https://bitbucket.org/ned/coveragepy/issues/484/multiprocessing-greenlet-concurrency -.. _issue 492: https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of -.. _issue 495: https://bitbucket.org/ned/coveragepy/issues/495/branch-and-concurrency-are-conflicting -.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins - - -.. _changes_41: - -Version 4.1 --- 2016-05-21 --------------------------- - -- Branch analysis has been rewritten: it used to be based on bytecode, but now - uses AST analysis. This has changed a number of things: - - - More code paths are now considered runnable, especially in - ``try``/``except`` structures. This may mean that coverage.py will - identify more code paths as uncovered. This could either raise or lower - your overall coverage number. - - - Python 3.5's ``async`` and ``await`` keywords are properly supported, - fixing `issue 434`_. - - - Missing branches reported with ``coverage report -m`` will now say - ``->exit`` for missed branches to the exit of a function, rather than a - negative number. Fixes `issue 469`_. - - - Some long-standing branch coverage bugs were fixed: - - - `issue 129`_: functions with only a docstring for a body would - incorrectly report a missing branch on the ``def`` line. - - - `issue 212`_: code in an ``except`` block could be incorrectly marked as - a missing branch. - - - `issue 146`_: context managers (``with`` statements) in a loop or ``try`` - block could confuse the branch measurement, reporting incorrect partial - branches. - - - `issue 422`_: in Python 3.5, an actual partial branch could be marked as - complete. - - - During branch coverage of single-line callables like lambdas and - generator expressions, coverage.py can now distinguish between them never - being called, or being called but not completed. Fixes `issue 90`_, - `issue 460`_ and `issue 475`_. - -- Pragmas to disable coverage measurement can now be used on decorator lines, - and they will apply to the entire function or class being decorated. This - implements the feature requested in `issue 131`_. - -- Multiprocessing support is now available on Windows. Thanks, Rodrigue - Cloutier. - -- The HTML report has a few changes: - - - The HTML report now has a map of the file along the rightmost edge of the - page, giving an overview of where the missed lines are. Thanks, Dmitry - Shishov. - - - The HTML report now uses different monospaced fonts, favoring Consolas over - Courier. Along the way, `issue 472`_ about not properly handling one-space - indents was fixed. The index page also has slightly different styling, to - try to make the clickable detail pages more apparent. - -- The XML report now produces correct package names for modules found in - directories specified with ``source=``. Fixes `issue 465`_. - -- ``coverage --help`` and ``coverage --version`` now mention which tracer is - installed, to help diagnose problems. The docs mention which features need - the C extension. (`issue 479`_) - -- The `Coverage.report` function had two parameters with non-None defaults, - which have been changed. `show_missing` used to default to True, but now - defaults to None. If you had been calling `Coverage.report` without - specifying `show_missing`, you'll need to explicitly set it to True to keep - the same behavior. `skip_covered` used to default to False. It is now None, - which doesn't change the behavior. This fixes `issue 485`_. - -- It's never been possible to pass a namespace module to one of the analysis - functions, but now at least we raise a more specific error message, rather - than getting confused. (`issue 456`_) - -- The `coverage.process_startup` function now returns the `Coverage` instance - it creates, as suggested in `issue 481`_. - -.. _issue 90: https://bitbucket.org/ned/coveragepy/issues/90/lambda-expression-confuses-branch -.. _issue 129: https://bitbucket.org/ned/coveragepy/issues/129/misleading-branch-coverage-of-empty -.. _issue 131: https://bitbucket.org/ned/coveragepy/issues/131/pragma-on-a-decorator-line-should-affect -.. _issue 146: https://bitbucket.org/ned/coveragepy/issues/146/context-managers-confuse-branch-coverage -.. _issue 212: https://bitbucket.org/ned/coveragepy/issues/212/coverage-erroneously-reports-partial -.. _issue 422: https://bitbucket.org/ned/coveragepy/issues/422/python35-partial-branch-marked-as-fully -.. _issue 434: https://bitbucket.org/ned/coveragepy/issues/434/indexerror-in-python-35 -.. _issue 456: https://bitbucket.org/ned/coveragepy/issues/456/coverage-breaks-with-implicit-namespaces -.. _issue 460: https://bitbucket.org/ned/coveragepy/issues/460/confusing-html-report-for-certain-partial -.. _issue 461: https://bitbucket.org/ned/coveragepy/issues/461/multiline-asserts-need-too-many-pragma -.. _issue 465: https://bitbucket.org/ned/coveragepy/issues/465/coveragexml-produces-package-names-with-an -.. _issue 469: https://bitbucket.org/ned/coveragepy/issues/469/strange-1-line-number-in-branch-coverage -.. _issue 472: https://bitbucket.org/ned/coveragepy/issues/472/html-report-indents-incorrectly-for-one -.. _issue 475: https://bitbucket.org/ned/coveragepy/issues/475/generator-expression-is-marked-as-not -.. _issue 479: https://bitbucket.org/ned/coveragepy/issues/479/clarify-the-need-for-the-c-extension -.. _issue 481: https://bitbucket.org/ned/coveragepy/issues/481/asyncioprocesspoolexecutor-tracing-not -.. _issue 485: https://bitbucket.org/ned/coveragepy/issues/485/coveragereport-ignores-show_missing-and - - -.. _changes_403: - -Version 4.0.3 --- 2015-11-24 ----------------------------- - -- Fixed a mysterious problem that manifested in different ways: sometimes - hanging the process (`issue 420`_), sometimes making database connections - fail (`issue 445`_). - -- The XML report now has correct ``<source>`` elements when using a - ``--source=`` option somewhere besides the current directory. This fixes - `issue 439`_. Thanks, Arcady Ivanov. - -- Fixed an unusual edge case of detecting source encodings, described in - `issue 443`_. - -- Help messages that mention the command to use now properly use the actual - command name, which might be different than "coverage". Thanks to Ben - Finney, this closes `issue 438`_. - -.. _issue 420: https://bitbucket.org/ned/coveragepy/issues/420/coverage-40-hangs-indefinitely-on-python27 -.. _issue 438: https://bitbucket.org/ned/coveragepy/issues/438/parameterise-coverage-command-name -.. _issue 439: https://bitbucket.org/ned/coveragepy/issues/439/incorrect-cobertura-file-sources-generated -.. _issue 443: https://bitbucket.org/ned/coveragepy/issues/443/coverage-gets-confused-when-encoding -.. _issue 445: https://bitbucket.org/ned/coveragepy/issues/445/django-app-cannot-connect-to-cassandra - - -.. _changes_402: - -Version 4.0.2 --- 2015-11-04 ----------------------------- - -- More work on supporting unusually encoded source. Fixed `issue 431`_. - -- Files or directories with non-ASCII characters are now handled properly, - fixing `issue 432`_. - -- Setting a trace function with sys.settrace was broken by a change in 4.0.1, - as reported in `issue 436`_. This is now fixed. - -- Officially support PyPy 4.0, which required no changes, just updates to the - docs. - -.. _issue 431: https://bitbucket.org/ned/coveragepy/issues/431/couldnt-parse-python-file-with-cp1252 -.. _issue 432: https://bitbucket.org/ned/coveragepy/issues/432/path-with-unicode-characters-various -.. _issue 436: https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from - - -.. _changes_401: - -Version 4.0.1 --- 2015-10-13 ----------------------------- - -- When combining data files, unreadable files will now generate a warning - instead of failing the command. This is more in line with the older - coverage.py v3.7.1 behavior, which silently ignored unreadable files. - Prompted by `issue 418`_. - -- The --skip-covered option would skip reporting on 100% covered files, but - also skipped them when calculating total coverage. This was wrong, it should - only remove lines from the report, not change the final answer. This is now - fixed, closing `issue 423`_. - -- In 4.0, the data file recorded a summary of the system on which it was run. - Combined data files would keep all of those summaries. This could lead to - enormous data files consisting of mostly repetitive useless information. That - summary is now gone, fixing `issue 415`_. If you want summary information, - get in touch, and we'll figure out a better way to do it. - -- Test suites that mocked os.path.exists would experience strange failures, due - to coverage.py using their mock inadvertently. This is now fixed, closing - `issue 416`_. - -- Importing a ``__init__`` module explicitly would lead to an error: - ``AttributeError: 'module' object has no attribute '__path__'``, as reported - in `issue 410`_. This is now fixed. - -- Code that uses ``sys.settrace(sys.gettrace())`` used to incur a more than 2x - speed penalty. Now there's no penalty at all. Fixes `issue 397`_. - -- Pyexpat C code will no longer be recorded as a source file, fixing - `issue 419`_. - -- The source kit now contains all of the files needed to have a complete source - tree, re-fixing `issue 137`_ and closing `issue 281`_. - -.. _issue 281: https://bitbucket.org/ned/coveragepy/issues/281/supply-scripts-for-testing-in-the -.. _issue 397: https://bitbucket.org/ned/coveragepy/issues/397/stopping-and-resuming-coverage-with -.. _issue 410: https://bitbucket.org/ned/coveragepy/issues/410/attributeerror-module-object-has-no -.. _issue 415: https://bitbucket.org/ned/coveragepy/issues/415/repeated-coveragedataupdates-cause -.. _issue 416: https://bitbucket.org/ned/coveragepy/issues/416/mocking-ospathexists-causes-failures -.. _issue 418: https://bitbucket.org/ned/coveragepy/issues/418/json-parse-error -.. _issue 419: https://bitbucket.org/ned/coveragepy/issues/419/nosource-no-source-for-code-path-to-c -.. _issue 423: https://bitbucket.org/ned/coveragepy/issues/423/skip_covered-changes-reported-total - - -.. _changes_40: - -Version 4.0 --- 2015-09-20 --------------------------- - - -Backward incompatibilities: - -- Python versions supported are now: - - - CPython 2.6, 2.7, 3.3, 3.4 and 3.5 - - PyPy2 2.4, 2.6 - - PyPy3 2.4 - -- The original command line switches (``-x`` to run a program, etc) are no - longer supported. - -- The ``COVERAGE_OPTIONS`` environment variable is no longer supported. It was - a hack for ``--timid`` before configuration files were available. - -- The original module-level function interface to coverage.py is no longer - supported. You must now create a :class:`coverage.Coverage` object, and use - methods on it. - -- The ``Coverage.use_cache`` method is no longer supported. - -- The private method ``Coverage._harvest_data`` is now called - :meth:`Coverage.get_data`, and returns the :class:`CoverageData` containing - the collected data. - -- Coverage.py is now licensed under the Apache 2.0 license. See `NOTICE.txt`_ - for details. - -- Coverage.py kits no longer include tests and docs. If you were using them, - get in touch and let me know how. - -.. _NOTICE.txt: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - - -Major new features: - -- Plugins: third parties can write plugins to add file support for non-Python - files, such as web application templating engines, or languages that compile - down to Python. See :ref:`plugins` for how to use plugins, and - :ref:`api_plugin` for details of how to write them. A plugin for measuring - Django template coverage is available: `django_coverage_plugin`_ - -- Gevent, eventlet, and greenlet are now supported. The ``[run] concurrency`` - setting, or the ``--concurrency`` command line switch, specifies the - concurrency library in use. Huge thanks to Peter Portante for initial - implementation, and to Joe Jevnik for the final insight that completed the - work. - -- The data storage has been re-written, using JSON instead of pickle. The - :class:`.CoverageData` class is a new supported API to the contents of the - data file. Data files from older versions of coverage.py can be converted to - the new format with ``python -m coverage.pickle2json``. - -- Wildly experimental: support for measuring processes started by the - multiprocessing module. To use, set ``--concurrency=multiprocessing``, - either on the command line or in the .coveragerc file. Thanks, Eduardo - Schettino. Currently, this does not work on Windows. - - -New features: - -- Options are now also read from a setup.cfg file, if any. Sections are - prefixed with "coverage:", so the ``[run]`` options will be read from the - ``[coverage:run]`` section of setup.cfg. - -- The HTML report now has filtering. Type text into the Filter box on the - index page, and only modules with that text in the name will be shown. - Thanks, Danny Allen. - -- A new option: ``coverage report --skip-covered`` - (or ``[report] skip_covered``) will reduce the number of files reported by - skipping files with 100% coverage. Thanks, Krystian Kichewko. This means - that empty ``__init__.py`` files will be skipped, since they are 100% - covered. - -- You can now specify the ``--fail-under`` option in the ``.coveragerc`` file - as the ``[report] fail_under`` option. - -- The ``report -m`` command now shows missing branches when reporting on branch - coverage. Thanks, Steve Leonard. - -- The ``coverage combine`` command now accepts any number of directories or - files as arguments, and will combine all the data from them. This means you - don't have to copy the files to one directory before combining. Thanks, - Christine Lytwynec. - -- A new configuration option for the XML report: ``[xml] package_depth`` - controls which directories are identified as packages in the report. - Directories deeper than this depth are not reported as packages. - The default is that all directories are reported as packages. - Thanks, Lex Berezhny. - -- A new configuration option, ``[run] note``, lets you set a note that will be - stored in the ``runs`` section of the data file. You can use this to - annotate the data file with any information you like. - -- The COVERAGE_DEBUG environment variable can be used to set the - ``[run] debug`` configuration option to control what internal operations are - logged. - -- A new version identifier is available, `coverage.version_info`, a plain tuple - of values similar to `sys.version_info`_. - - -Improvements: - -- Coverage.py now always adds the current directory to sys.path, so that - plugins can import files in the current directory. - -- Coverage.py now accepts a directory name for ``coverage run`` and will run a - ``__main__.py`` found there, just like Python will. Thanks, Dmitry Trofimov. - -- The ``--debug`` switch can now be used on any command. - -- Reports now use file names with extensions. Previously, a report would - describe a/b/c.py as "a/b/c". Now it is shown as "a/b/c.py". This allows - for better support of non-Python files. - -- Missing branches in the HTML report now have a bit more information in the - right-hand annotations. Hopefully this will make their meaning clearer. - -- The XML report now contains a <source> element. Thanks Stan Hu. - -- The XML report now includes a ``missing-branches`` attribute. Thanks, Steve - Peak. This is not a part of the Cobertura DTD, so the XML report no longer - references the DTD. - -- The XML report now reports each directory as a package again. This was a bad - regression, I apologize. - -- In parallel mode, ``coverage erase`` will now delete all of the data files. - -- A new warning is possible, if a desired file isn't measured because it was - imported before coverage.py was started. - -- The :func:`coverage.process_startup` function now will start coverage - measurement only once, no matter how many times it is called. This fixes - problems due to unusual virtualenv configurations. - -- Unrecognized configuration options will now print an error message and stop - coverage.py. This should help prevent configuration mistakes from passing - silently. - - -API changes: - -- The class defined in the coverage module is now called ``Coverage`` instead - of ``coverage``, though the old name still works, for backward compatibility. - -- You can now programmatically adjust the configuration of coverage.py by - calling :meth:`Coverage.set_option` after construction. - :meth:`Coverage.get_option` reads the configuration values. - -- If the `config_file` argument to the Coverage constructor is specified as - ".coveragerc", it is treated as if it were True. This means setup.cfg is - also examined, and a missing file is not considered an error. - - -Bug fixes: - -- The textual report and the HTML report used to report partial branches - differently for no good reason. Now the text report's "missing branches" - column is a "partial branches" column so that both reports show the same - numbers. This closes `issue 342`_. - -- The ``fail-under`` value is now rounded the same as reported results, - preventing paradoxical results, fixing `issue 284`_. - -- Branch coverage couldn't properly handle certain extremely long files. This - is now fixed, closing `issue 359`_. - -- Branch coverage didn't understand yield statements properly. Mickie Betz - persisted in pursuing this despite Ned's pessimism. Fixes `issue 308`_ and - `issue 324`_. - -- Files with incorrect encoding declaration comments are no longer ignored by - the reporting commands. - -- Empty files are now reported as 100% covered in the XML report, not 0% - covered. - -- The XML report will now create the output directory if need be. Thanks, Chris - Rose. - -- HTML reports no longer raise UnicodeDecodeError if a Python file has - undecodable characters. - -- The annotate command will now annotate all files, not just ones relative to - the current directory. - - -.. _django_coverage_plugin: https://pypi.python.org/pypi/django_coverage_plugin -.. _issue 284: https://bitbucket.org/ned/coveragepy/issue/284/fail-under-should-show-more-precision -.. _issue 308: https://bitbucket.org/ned/coveragepy/issue/308/yield-lambda-branch-coverage -.. _issue 324: https://bitbucket.org/ned/coveragepy/issue/324/yield-in-loop-confuses-branch-coverage -.. _issue 342: https://bitbucket.org/ned/coveragepy/issue/342/console-and-html-coverage-reports-differ -.. _issue 359: https://bitbucket.org/ned/coveragepy/issue/359/xml-report-chunk-error -.. _sys.version_info: https://docs.python.org/3/library/sys.html#sys.version_info - - -.. _changes_371: - -Version 3.7.1 --- 2013-12-13 ----------------------------- - -- Improved the speed of HTML report generation by about 20%. - -- Fixed the mechanism for finding OS-installed static files for the HTML report - so that it will actually find OS-installed static files. - - -.. _changes_37: - -Version 3.7 --- 2013-10-06 --------------------------- - -- Added the ``--debug`` switch to ``coverage run``. It accepts a list of - options indicating the type of internal activity to log to stderr. For - details, see :ref:`the run --debug options <cmd_run_debug>`. - -- Improved the branch coverage facility, fixing `issue 92`_ and `issue 175`_. - -- Running code with ``coverage run -m`` now behaves more like Python does, - setting sys.path properly, which fixes `issue 207`_ and `issue 242`_. - -- Coverage.py can now run .pyc files directly, closing `issue 264`_. - -- Coverage.py properly supports .pyw files, fixing `issue 261`_. - -- Omitting files within a tree specified with the ``source`` option would - cause them to be incorrectly marked as unexecuted, as described in - `issue 218`_. This is now fixed. - -- When specifying paths to alias together during data combining, you can now - specify relative paths, fixing `issue 267`_. - -- Most file paths can now be specified with username expansion (``~/src``, or - ``~build/src``, for example), and with environment variable expansion - (``build/$BUILDNUM/src``). - -- Trying to create an XML report with no files to report on, would cause a - ZeroDivideError, but no longer does, fixing `issue 250`_. - -- When running a threaded program under the Python tracer, coverage.py no - longer issues a spurious warning about the trace function changing: "Trace - function changed, measurement is likely wrong: None." This fixes - `issue 164`_. - -- Static files necessary for HTML reports are found in system-installed places, - to ease OS-level packaging of coverage.py. Closes `issue 259`_. - -- Source files with encoding declarations, but a blank first line, were not - decoded properly. Now they are. Thanks, Roger Hu. - -- The source kit now includes the ``__main__.py`` file in the root coverage - directory, fixing `issue 255`_. - -.. _issue 92: https://bitbucket.org/ned/coveragepy/issue/92/finally-clauses-arent-treated-properly-in -.. _issue 164: https://bitbucket.org/ned/coveragepy/issue/164/trace-function-changed-warning-when-using -.. _issue 175: https://bitbucket.org/ned/coveragepy/issue/175/branch-coverage-gets-confused-in-certain -.. _issue 207: https://bitbucket.org/ned/coveragepy/issue/207/run-m-cannot-find-module-or-package-in -.. _issue 242: https://bitbucket.org/ned/coveragepy/issue/242/running-a-two-level-package-doesnt-work -.. _issue 218: https://bitbucket.org/ned/coveragepy/issue/218/run-command-does-not-respect-the-omit-flag -.. _issue 250: https://bitbucket.org/ned/coveragepy/issue/250/uncaught-zerodivisionerror-when-generating -.. _issue 255: https://bitbucket.org/ned/coveragepy/issue/255/directory-level-__main__py-not-included-in -.. _issue 259: https://bitbucket.org/ned/coveragepy/issue/259/allow-use-of-system-installed-third-party -.. _issue 261: https://bitbucket.org/ned/coveragepy/issue/261/pyw-files-arent-reported-properly -.. _issue 264: https://bitbucket.org/ned/coveragepy/issue/264/coverage-wont-run-pyc-files -.. _issue 267: https://bitbucket.org/ned/coveragepy/issue/267/relative-path-aliases-dont-work - - -.. _changes_36: - -Version 3.6 --- 2013-01-05 --------------------------- - -Features: - -- The **report**, **html**, and **xml** commands now accept a ``--fail-under`` - switch that indicates in the exit status whether the coverage percentage was - less than a particular value. Closes `issue 139`_. - -- The reporting functions coverage.report(), coverage.html_report(), and - coverage.xml_report() now all return a float, the total percentage covered - measurement. - -- The HTML report's title can now be set in the configuration file, with the - ``--title`` switch on the command line, or via the API. - -- Configuration files now support substitution of environment variables, using - syntax like ``${WORD}``. Closes `issue 97`_. - -Packaging: - -- The C extension is optionally compiled using a different more widely-used - technique, taking another stab at fixing `issue 80`_ once and for all. - -- When installing, now in addition to creating a "coverage" command, two new - aliases are also installed. A "coverage2" or "coverage3" command will be - created, depending on whether you are installing in Python 2.x or 3.x. - A "coverage-X.Y" command will also be created corresponding to your specific - version of Python. Closes `issue 111`_. - -- The coverage.py installer no longer tries to bootstrap setuptools or - Distribute. You must have one of them installed first, as `issue 202`_ - recommended. - -- The coverage.py kit now includes docs (closing `issue 137`_) and tests. - -Docs: - -- Added a page to the docs about :doc:`contributing <contributing>` to - coverage.py, closing `issue 171`_. - -- Added a page to the docs about :doc:`troublesome situations <trouble>`, - closing `issue 226`_. - -- Docstrings for the legacy singleton methods are more helpful. Thanks Marius - Gedminas. Closes `issue 205`_. - -- The pydoc tool can now show documentation for the class `coverage.coverage`. - Closes `issue 206`_. - -- Added some info to the TODO file, closing `issue 227`_. - -Fixes: - -- Wildcards in ``include=`` and ``omit=`` arguments were not handled properly - in reporting functions, though they were when running. Now they are handled - uniformly, closing `issue 143`_ and `issue 163`_. **NOTE**: it is possible - that your configurations may now be incorrect. If you use ``include`` or - ``omit`` during reporting, whether on the command line, through the API, or - in a configuration file, please check carefully that you were not relying on - the old broken behavior. - -- Embarrassingly, the `[xml] output=` setting in the .coveragerc file simply - didn't work. Now it does. - -- Combining data files would create entries for phantom files if used with - ``source`` and path aliases. It no longer does. - -- ``debug sys`` now shows the configuration file path that was read. - -- If an oddly-behaved package claims that code came from an empty-string - file name, coverage.py no longer associates it with the directory name, - fixing `issue 221`_. - -- The XML report now consistently uses file names for the filename attribute, - rather than sometimes using module names. Fixes `issue 67`_. - Thanks, Marcus Cobden. - -- Coverage percentage metrics are now computed slightly differently under - branch coverage. This means that completely unexecuted files will now - correctly have 0% coverage, fixing `issue 156`_. This also means that your - total coverage numbers will generally now be lower if you are measuring - branch coverage. - -- On Windows, files are now reported in their correct case, fixing `issue 89`_ - and `issue 203`_. - -- If a file is missing during reporting, the path shown in the error message - is now correct, rather than an incorrect path in the current directory. - Fixes `issue 60`_. - -- Running an HTML report in Python 3 in the same directory as an old Python 2 - HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_) - is now fixed. - -- Fixed yet another error trying to parse non-Python files as Python, this - time an IndentationError, closing `issue 82`_ for the fourth time... - -- If `coverage xml` fails because there is no data to report, it used to - create a zero-length XML file. Now it doesn't, fixing `issue 210`_. - -- Jython files now work with the ``--source`` option, fixing `issue 100`_. - -- Running coverage.py under a debugger is unlikely to work, but it shouldn't - fail with "TypeError: 'NoneType' object is not iterable". Fixes - `issue 201`_. - -- On some Linux distributions, when installed with the OS package manager, - coverage.py would report its own code as part of the results. Now it won't, - fixing `issue 214`_, though this will take some time to be repackaged by the - operating systems. - -- When coverage.py ended unsuccessfully, it may have reported odd errors like - ``'NoneType' object has no attribute 'isabs'``. It no longer does, - so kiss `issue 153`_ goodbye. - - -.. _issue 60: https://bitbucket.org/ned/coveragepy/issue/60/incorrect-path-to-orphaned-pyc-files -.. _issue 67: https://bitbucket.org/ned/coveragepy/issue/67/xml-report-filenames-may-be-generated -.. _issue 80: https://bitbucket.org/ned/coveragepy/issue/80/is-there-a-duck-typing-way-to-know-we-cant -.. _issue 89: https://bitbucket.org/ned/coveragepy/issue/89/on-windows-all-packages-are-reported-in -.. _issue 97: https://bitbucket.org/ned/coveragepy/issue/97/allow-environment-variables-to-be -.. _issue 100: https://bitbucket.org/ned/coveragepy/issue/100/source-directive-doesnt-work-for-packages -.. _issue 111: https://bitbucket.org/ned/coveragepy/issue/111/when-installing-coverage-with-pip-not -.. _issue 137: https://bitbucket.org/ned/coveragepy/issue/137/provide-docs-with-source-distribution -.. _issue 139: https://bitbucket.org/ned/coveragepy/issue/139/easy-check-for-a-certain-coverage-in-tests -.. _issue 143: https://bitbucket.org/ned/coveragepy/issue/143/omit-doesnt-seem-to-work-in-coverage -.. _issue 153: https://bitbucket.org/ned/coveragepy/issue/153/non-existent-filename-triggers -.. _issue 156: https://bitbucket.org/ned/coveragepy/issue/156/a-completely-unexecuted-file-shows-14 -.. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename -.. _issue 171: https://bitbucket.org/ned/coveragepy/issue/171/how-to-contribute-and-run-tests -.. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy -.. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on -.. _issue 202: https://bitbucket.org/ned/coveragepy/issue/202/get-rid-of-ez_setuppy-and -.. _issue 203: https://bitbucket.org/ned/coveragepy/issue/203/duplicate-filenames-reported-when-filename -.. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly -.. _issue 206: https://bitbucket.org/ned/coveragepy/issue/206/pydoc-coveragecoverage-fails-with-an-error -.. _issue 210: https://bitbucket.org/ned/coveragepy/issue/210/if-theres-no-coverage-data-coverage-xml -.. _issue 214: https://bitbucket.org/ned/coveragepy/issue/214/coveragepy-measures-itself-on-precise -.. _issue 221: https://bitbucket.org/ned/coveragepy/issue/221/coveragepy-incompatible-with-pyratemp -.. _issue 226: https://bitbucket.org/ned/coveragepy/issue/226/make-readme-section-to-describe-when -.. _issue 227: https://bitbucket.org/ned/coveragepy/issue/227/update-todo - - -.. _changes_353: - -Version 3.5.3 --- 2012-09-29 ----------------------------- - -- Line numbers in the HTML report line up better with the source lines, fixing - `issue 197`_, thanks Marius Gedminas. - -- When specifying a directory as the source= option, the directory itself no - longer needs to have a ``__init__.py`` file, though its sub-directories do, - to be considered as source files. - -- Files encoded as UTF-8 with a BOM are now properly handled, fixing - `issue 179`_. Thanks, Pablo Carballo. - -- Fixed more cases of non-Python files being reported as Python source, and - then not being able to parse them as Python. Closes `issue 82`_ (again). - Thanks, Julian Berman. - -- Fixed memory leaks under Python 3, thanks, Brett Cannon. Closes `issue 147`_. - -- Optimized .pyo files may not have been handled correctly, `issue 195`_. - Thanks, Marius Gedminas. - -- Certain unusually named file paths could have been mangled during reporting, - `issue 194`_. Thanks, Marius Gedminas. - -- Try to do a better job of the impossible task of detecting when we can't - build the C extension, fixing `issue 183`_. - -.. _issue 147: https://bitbucket.org/ned/coveragepy/issue/147/massive-memory-usage-by-ctracer -.. _issue 179: https://bitbucket.org/ned/coveragepy/issue/179/htmlreporter-fails-when-source-file-is -.. _issue 183: https://bitbucket.org/ned/coveragepy/issue/183/install-fails-for-python-23 -.. _issue 194: https://bitbucket.org/ned/coveragepy/issue/194/filelocatorrelative_filename-could-mangle -.. _issue 195: https://bitbucket.org/ned/coveragepy/issue/195/pyo-file-handling-in-codeunit -.. _issue 197: https://bitbucket.org/ned/coveragepy/issue/197/line-numbers-in-html-report-do-not-align - - -.. _changes_352: - -Version 3.5.2 --- 2012-05-04 ----------------------------- - -- The HTML report has slightly tweaked controls: the buttons at the top of - the page are color-coded to the source lines they affect. - -- Custom CSS can be applied to the HTML report by specifying a CSS file as - the extra_css configuration value in the [html] section. - -- Source files with custom encodings declared in a comment at the top are now - properly handled during reporting on Python 2. Python 3 always handled them - properly. This fixes `issue 157`_. - -- Backup files left behind by editors are no longer collected by the source= - option, fixing `issue 168`_. - -- If a file doesn't parse properly as Python, we don't report it as an error - if the file name seems like maybe it wasn't meant to be Python. This is a - pragmatic fix for `issue 82`_. - -- The ``-m`` switch on ``coverage report``, which includes missing line numbers - in the summary report, can now be specified as ``show_missing`` in the - config file. Closes `issue 173`_. - -- When running a module with ``coverage run -m <modulename>``, certain details - of the execution environment weren't the same as for - ``python -m <modulename>``. This had the unfortunate side-effect of making - ``coverage run -m unittest discover`` not work if you had tests in a - directory named "test". This fixes `issue 155`_. - -- Now the exit status of your product code is properly used as the process - status when running ``python -m coverage run ...``. Thanks, JT Olds. - -- When installing into pypy, we no longer attempt (and fail) to compile - the C tracer function, closing `issue 166`_. - -.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report -.. _issue 155: https://bitbucket.org/ned/coveragepy/issue/155/cant-use-coverage-run-m-unittest-discover -.. _issue 157: https://bitbucket.org/ned/coveragepy/issue/157/chokes-on-source-files-with-non-utf-8 -.. _issue 166: https://bitbucket.org/ned/coveragepy/issue/166/dont-try-to-compile-c-extension-on-pypy -.. _issue 168: https://bitbucket.org/ned/coveragepy/issue/168/dont-be-alarmed-by-emacs-droppings -.. _issue 173: https://bitbucket.org/ned/coveragepy/issue/173/theres-no-way-to-specify-show-missing-in - - -.. _changes_351: - -Version 3.5.1 --- 2011-09-23 ----------------------------- - -- When combining data files from parallel runs, you can now instruct - coverage.py about which directories are equivalent on different machines. A - ``[paths]`` section in the configuration file lists paths that are to be - considered equivalent. Finishes `issue 17`_. - -- for-else constructs are understood better, and don't cause erroneous partial - branch warnings. Fixes `issue 122`_. - -- Branch coverage for ``with`` statements is improved, fixing `issue 128`_. - -- The number of partial branches reported on the HTML summary page was - different than the number reported on the individual file pages. This is - now fixed. - -- An explicit include directive to measure files in the Python installation - wouldn't work because of the standard library exclusion. Now the include - directive takes precedence, and the files will be measured. Fixes - `issue 138`_. - -- The HTML report now handles Unicode characters in Python source files - properly. This fixes `issue 124`_ and `issue 144`_. Thanks, Devin - Jeanpierre. - -- In order to help the core developers measure the test coverage of the - standard library, Brandon Rhodes devised an aggressive hack to trick Python - into running some coverage.py code before anything else in the process. - See the coverage/fullcoverage directory if you are interested. - -.. _issue 17: http://bitbucket.org/ned/coveragepy/issue/17/support-combining-coverage-data-from -.. _issue 122: http://bitbucket.org/ned/coveragepy/issue/122/for-else-always-reports-missing-branch -.. _issue 124: http://bitbucket.org/ned/coveragepy/issue/124/no-arbitrary-unicode-in-html-reports-in -.. _issue 128: http://bitbucket.org/ned/coveragepy/issue/128/branch-coverage-of-with-statement-in-27 -.. _issue 138: http://bitbucket.org/ned/coveragepy/issue/138/include-should-take-precedence-over-is -.. _issue 144: http://bitbucket.org/ned/coveragepy/issue/144/failure-generating-html-output-for - - -.. _changes_35: - -Version 3.5 --- 2011-06-29 --------------------------- - -HTML reporting: - -- The HTML report now has hotkeys. Try ``n``, ``s``, ``m``, ``x``, ``b``, - ``p``, and ``c`` on the overview page to change the column sorting. - On a file page, ``r``, ``m``, ``x``, and ``p`` toggle the run, missing, - excluded, and partial line markings. You can navigate the highlighted - sections of code by using the ``j`` and ``k`` keys for next and previous. - The ``1`` (one) key jumps to the first highlighted section in the file, - and ``0`` (zero) scrolls to the top of the file. - -- HTML reporting is now incremental: a record is kept of the data that - produced the HTML reports, and only files whose data has changed will - be generated. This should make most HTML reporting faster. - - -Running Python files - -- Modules can now be run directly using ``coverage run -m modulename``, to - mirror Python's ``-m`` flag. Closes `issue 95`_, thanks, Brandon Rhodes. - -- ``coverage run`` didn't emulate Python accurately in one detail: the - current directory inserted into ``sys.path`` was relative rather than - absolute. This is now fixed. - -- Pathological code execution could disable the trace function behind our - backs, leading to incorrect code measurement. Now if this happens, - coverage.py will issue a warning, at least alerting you to the problem. - Closes `issue 93`_. Thanks to Marius Gedminas for the idea. - -- The C-based trace function now behaves properly when saved and restored - with ``sys.gettrace()`` and ``sys.settrace()``. This fixes `issue 125`_ - and `issue 123`_. Thanks, Devin Jeanpierre. - -- Coverage.py can now be run directly from a working tree by specifying - the directory name to python: ``python coverage_py_working_dir run ...``. - Thanks, Brett Cannon. - -- A little bit of Jython support: `coverage run` can now measure Jython - execution by adapting when $py.class files are traced. Thanks, Adi Roiban. - - -Reporting - -- Partial branch warnings can now be pragma'd away. The configuration option - ``partial_branches`` is a list of regular expressions. Lines matching any of - those expressions will never be marked as a partial branch. In addition, - there's a built-in list of regular expressions marking statements which - should never be marked as partial. This list includes ``while True:``, - ``while 1:``, ``if 1:``, and ``if 0:``. - -- The ``--omit`` and ``--include`` switches now interpret their values more - usefully. If the value starts with a wildcard character, it is used as-is. - If it does not, it is interpreted relative to the current directory. - Closes `issue 121`_. - -- Syntax errors in supposed Python files can now be ignored during reporting - with the ``-i`` switch just like other source errors. Closes `issue 115`_. - -.. _issue 93: http://bitbucket.org/ned/coveragepy/issue/93/copying-a-mock-object-breaks-coverage -.. _issue 95: https://bitbucket.org/ned/coveragepy/issue/95/run-subcommand-should-take-a-module-name -.. _issue 115: https://bitbucket.org/ned/coveragepy/issue/115/fail-gracefully-when-reporting-on-file -.. _issue 121: https://bitbucket.org/ned/coveragepy/issue/121/filename-patterns-are-applied-stupidly -.. _issue 123: https://bitbucket.org/ned/coveragepy/issue/123/pyeval_settrace-used-in-way-that-breaks -.. _issue 125: https://bitbucket.org/ned/coveragepy/issue/125/coverage-removes-decoratortoolss-tracing - - -.. _changes_34: - -Version 3.4 --- 2010-09-19 --------------------------- - -Controlling source: - -- BACKWARD INCOMPATIBILITY: the ``--omit`` and ``--include`` switches now take - file patterns rather than file prefixes, closing `issue 34`_ and `issue 36`_. - -- BACKWARD INCOMPATIBILITY: the `omit_prefixes` argument is gone throughout - coverage.py, replaced with `omit`, a list of file name patterns suitable for - `fnmatch`. A parallel argument `include` controls what files are included. - -- The run command now has a ``--source`` switch, a list of directories or - module names. If provided, coverage.py will only measure execution in those - source files. The run command also now supports ``--include`` and ``--omit`` - to control what modules it measures. This can speed execution and reduce the - amount of data during reporting. Thanks Zooko. - -- The reporting commands (report, annotate, html, and xml) now have an - ``--include`` switch to restrict reporting to modules matching those file - patterns, similar to the existing ``--omit`` switch. Thanks, Zooko. - -Reporting: - -- Completely unexecuted files can now be included in coverage results, reported - as 0% covered. This only happens if the --source option is specified, since - coverage.py needs guidance about where to look for source files. - -- Python files with no statements, for example, empty ``__init__.py`` files, - are now reported as having zero statements instead of one. Fixes `issue 1`_. - -- Reports now have a column of missed line counts rather than executed line - counts, since developers should focus on reducing the missed lines to zero, - rather than increasing the executed lines to varying targets. Once - suggested, this seemed blindingly obvious. - -- Coverage percentages are now displayed uniformly across reporting methods. - Previously, different reports could round percentages differently. Also, - percentages are only reported as 0% or 100% if they are truly 0 or 100, and - are rounded otherwise. Fixes `issue 41`_ and `issue 70`_. - -- The XML report output now properly includes a percentage for branch coverage, - fixing `issue 65`_ and `issue 81`_, and the report is sorted by package - name, fixing `issue 88`_. - -- The XML report is now sorted by package name, fixing `issue 88`_. - -- The precision of reported coverage percentages can be set with the - ``[report] precision`` config file setting. Completes `issue 16`_. - -- Line numbers in HTML source pages are clickable, linking directly to that - line, which is highlighted on arrival. Added a link back to the index page - at the bottom of each HTML page. - -Execution and measurement: - -- Various warnings are printed to stderr for problems encountered during data - measurement: if a ``--source`` module has no Python source to measure, or is - never encountered at all, or if no data is collected. - -- Doctest text files are no longer recorded in the coverage data, since they - can't be reported anyway. Fixes `issue 52`_ and `issue 61`_. - -- Threads derived from ``threading.Thread`` with an overridden `run` method - would report no coverage for the `run` method. This is now fixed, closing - `issue 85`_. - -- Programs that exited with ``sys.exit()`` with no argument weren't handled - properly, producing a coverage.py stack trace. This is now fixed. - -- Programs that call ``os.fork`` will properly collect data from both the child - and parent processes. Use ``coverage run -p`` to get two data files that can - be combined with ``coverage combine``. Fixes `issue 56`_. - -- When measuring code running in a virtualenv, most of the system library was - being measured when it shouldn't have been. This is now fixed. - -- Coverage.py can now be run as a module: ``python -m coverage``. Thanks, - Brett Cannon. - -.. _issue 1: http://bitbucket.org/ned/coveragepy/issue/1/empty-__init__py-files-are-reported-as-1-executable -.. _issue 16: http://bitbucket.org/ned/coveragepy/issue/16/allow-configuration-of-accuracy-of-percentage-totals -.. _issue 34: http://bitbucket.org/ned/coveragepy/issue/34/enhanced-omit-globbing-handling -.. _issue 36: http://bitbucket.org/ned/coveragepy/issue/36/provide-regex-style-omit -.. _issue 41: http://bitbucket.org/ned/coveragepy/issue/41/report-says-100-when-it-isnt-quite-there -.. _issue 52: http://bitbucket.org/ned/coveragepy/issue/52/doctesttestfile-confuses-source-detection -.. _issue 56: http://bitbucket.org/ned/coveragepy/issue/56/coveragepy-cant-trace-child-processes-of-a -.. _issue 61: http://bitbucket.org/ned/coveragepy/issue/61/annotate-i-doesnt-work -.. _issue 65: http://bitbucket.org/ned/coveragepy/issue/65/branch-option-not-reported-in-cobertura -.. _issue 70: http://bitbucket.org/ned/coveragepy/issue/70/text-report-and-html-report-disagree-on-coverage -.. _issue 81: http://bitbucket.org/ned/coveragepy/issue/81/xml-report-does-not-have-condition-coverage-attribute-for-lines-with-a -.. _issue 85: http://bitbucket.org/ned/coveragepy/issue/85/threadrun-isnt-measured -.. _issue 88: http://bitbucket.org/ned/coveragepy/issue/88/xml-report-lists-packages-in-random-order - - -.. _changes_331: - -Version 3.3.1 --- 2010-03-06 ----------------------------- - -- Using ``parallel=True`` in a .coveragerc file prevented reporting, but now - does not, fixing `issue 49`_. - -- When running your code with ``coverage run``, if you call ``sys.exit()``, - coverage.py will exit with that status code, fixing `issue 50`_. - -.. _issue 49: http://bitbucket.org/ned/coveragepy/issue/49 -.. _issue 50: http://bitbucket.org/ned/coveragepy/issue/50 - - -.. _changes_33: - -Version 3.3 --- 2010-02-24 --------------------------- - -- Settings are now read from a .coveragerc file. A specific file can be - specified on the command line with ``--rcfile=FILE``. The name of the file - can be programmatically set with the ``config_file`` argument to the - coverage() constructor, or reading a config file can be disabled with - ``config_file=False``. - -- Added coverage.process_start to enable coverage measurement when Python - starts. - -- Parallel data file names now have a random number appended to them in - addition to the machine name and process id. Also, parallel data files - combined with ``coverage combine`` are deleted after they're combined, to - clean up unneeded files. Fixes `issue 40`_. - -- Exceptions thrown from product code run with ``coverage run`` are now - displayed without internal coverage.py frames, so the output is the same as - when the code is run without coverage.py. - -- Fixed `issue 39`_ and `issue 47`_. - -.. _issue 39: http://bitbucket.org/ned/coveragepy/issue/39 -.. _issue 40: http://bitbucket.org/ned/coveragepy/issue/40 -.. _issue 47: http://bitbucket.org/ned/coveragepy/issue/47 - - -.. _changes_32: - -Version 3.2 --- 2009-12-05 --------------------------- - -- Branch coverage: coverage.py can tell you which branches didn't have both (or - all) choices executed, even where the choice doesn't affect which lines were - executed. See :ref:`branch` for more details. - -- The table of contents in the HTML report is now sortable: click the headers - on any column. The sorting is persisted so that subsequent reports are - sorted as you wish. Thanks, `Chris Adams`_. - -- XML reporting has file paths that let Cobertura find the source code, fixing - `issue 21`_. - -- The ``--omit`` option now works much better than before, fixing `issue 14`_ - and `issue 33`_. Thanks, Danek Duvall. - -- Added a ``--version`` option on the command line. - -- Program execution under coverage.py is a few percent faster. - -- Some exceptions reported by the command line interface have been cleaned up - so that tracebacks inside coverage.py aren't shown. Fixes `issue 23`_. - -- Fixed some problems syntax coloring sources with line continuations and - source with tabs: `issue 30`_ and `issue 31`_. - -.. _Chris Adams: http://improbable.org/chris/ -.. _issue 21: http://bitbucket.org/ned/coveragepy/issue/21 -.. _issue 23: http://bitbucket.org/ned/coveragepy/issue/23 -.. _issue 14: http://bitbucket.org/ned/coveragepy/issue/14 -.. _issue 30: http://bitbucket.org/ned/coveragepy/issue/30 -.. _issue 31: http://bitbucket.org/ned/coveragepy/issue/31 -.. _issue 33: http://bitbucket.org/ned/coveragepy/issue/33 - - -.. _changes_31: - -Version 3.1 --- 2009-10-04 --------------------------- - -- Python 3.1 is now supported. - -- Coverage.py has a new command line syntax with sub-commands. This expands - the possibilities for adding features and options in the future. The old - syntax is still supported. Try ``coverage help`` to see the new commands. - Thanks to Ben Finney for early help. - -- Added an experimental ``coverage xml`` command for producing coverage reports - in a Cobertura-compatible XML format. Thanks, Bill Hart. - -- Added the ``--timid`` option to enable a simpler slower trace function that - works for DecoratorTools projects, including TurboGears. Fixed `issue 12`_ - and `issue 13`_. - -- HTML reports now display syntax-colored Python source. - -- Added a ``coverage debug`` command for getting diagnostic information about - the coverage.py installation. - -- Source code can now be read from eggs. Thanks, Ross Lawley. Fixes - `issue 25`_. - -.. _issue 25: http://bitbucket.org/ned/coveragepy/issue/25 -.. _issue 12: http://bitbucket.org/ned/coveragepy/issue/12 -.. _issue 13: http://bitbucket.org/ned/coveragepy/issue/13 - - -.. _changes_301: - -Version 3.0.1 --- 2009-07-07 ----------------------------- - -- Removed the recursion limit in the tracer function. Previously, code that - ran more than 500 frames deep would crash. - -- Fixed a bizarre problem involving pyexpat, whereby lines following XML parser - invocations could be overlooked. - -- On Python 2.3, coverage.py could mis-measure code with exceptions being - raised. This is now fixed. - -- The coverage.py code itself will now not be measured by coverage.py, and no - coverage.py modules will be mentioned in the nose ``--with-cover`` plugin. - -- When running source files, coverage.py now opens them in universal newline - mode just like Python does. This lets it run Windows files on Mac, for - example. - - -.. _changes_30: - -Version 3.0 --- 2009-06-13 --------------------------- - -- Coverage.py is now a package rather than a module. Functionality has been - split into classes. - -- HTML reports and annotation of source files: use the new ``-b`` (browser) - switch. Thanks to George Song for code, inspiration and guidance. - -- The trace function is implemented in C for speed. Coverage.py runs are now - much faster. Thanks to David Christian for productive micro-sprints and - other encouragement. - -- The minimum supported Python version is 2.3. - -- When using the object API (that is, constructing a coverage() object), data - is no longer saved automatically on process exit. You can re-enable it with - the ``auto_data=True`` parameter on the coverage() constructor. - The module-level interface still uses automatic saving. - -- Code in the Python standard library is not measured by default. If you need - to measure standard library code, use the ``-L`` command-line switch during - execution, or the ``cover_pylib=True`` argument to the coverage() - constructor. - -- API changes: - - - Added parameters to coverage.__init__ for options that had been set on - the coverage object itself. - - - Added clear_exclude() and get_exclude_list() methods for programmatic - manipulation of the exclude regexes. - - - Added coverage.load() to read previously-saved data from the data file. - - - coverage.annotate_file is no longer available. - - - Removed the undocumented cache_file argument to coverage.usecache(). +.. include:: ../CHANGES.rst diff --git a/doc/cmd.rst b/doc/cmd.rst index 6ca1fc4..e9d5100 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -131,40 +131,53 @@ If you are measuring coverage in a multi-process program, or across a number of machines, you'll want the ``--parallel-mode`` switch to keep the data separate during measurement. See :ref:`cmd_combining` below. + +.. _cmd_warnings: + +Warnings +-------- + During execution, coverage.py may warn you about conditions it detects that could affect the measurement process. The possible warnings include: -* "Trace function changed, measurement is likely wrong: XXX" +* "Trace function changed, measurement is likely wrong: XXX (trace-changed)" Coverage measurement depends on a Python setting called the trace function. Other Python code in your product might change that function, which will - disrupt coverage.py's measurement. This warning indicate that has happened. + disrupt coverage.py's measurement. This warning indicates that has happened. The XXX in the message is the new trace function value, which might provide a clue to the cause. -* "Module XXX has no Python source" +* "Module XXX has no Python source (module-not-python)" You asked coverage.py to measure module XXX, but once it was imported, it turned out not to have a corresponding .py file. Without a .py file, coverage.py can't report on missing lines. -* "Module XXX was never imported" +* "Module XXX was never imported (module-not-imported)" You asked coverage.py to measure module XXX, but it was never imported by your program. -* "No data was collected" +* "No data was collected (no-data-collected)" Coverage.py ran your program, but didn't measure any lines as executed. This could be because you asked to measure only modules that never ran, or for other reasons. -* "Module XXX was previously imported, but not measured." +* "Module XXX was previously imported, but not measured. (module-not-measured)" You asked coverage.py to measure module XXX, but it had already been imported when coverage started. This meant coverage.py couldn't monitor its execution. +Individual warnings can be disabled with the `disable_warnings +<config_run_disable_warnings>`_ configuration setting. To silence "No data was +collected," add this to your .coveragerc file:: + + [run] + disable_warnings = no-data-collected + .. _cmd_datafile: @@ -366,6 +379,8 @@ is a data file that is used to speed up reporting the next time. If you generate a new report into the same directory, coverage.py will skip generating unchanged pages, making the process faster. +The ``--skip-covered`` switch will leave out any file with 100% coverage, +letting you focus on the files that still need attention. .. _cmd_annotation: @@ -449,10 +464,15 @@ to log: * ``dataop``: log when data is added to the CoverageData object. -* ``pid``: annotate all debug output with the process id. +* ``multiproc``: log the start and stop of multiprocessing processes. + +* ``pid``: annotate all warnings and debug output with the process id. * ``plugin``: print information about plugin operations. +* ``process``: show process creation information, and changes in the current + directory. + * ``sys``: before starting, dump all the system and environment information, as with :ref:`coverage debug sys <cmd_debug>`. diff --git a/doc/conf.py b/doc/conf.py index 684628d..5d63eb1 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -49,16 +49,16 @@ master_doc = 'index' # General information about the project. project = u'Coverage.py' -copyright = u'2009\N{EN DASH}2016, Ned Batchelder' +copyright = u'2009\N{EN DASH}2017, Ned Batchelder' # CHANGEME # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '4.2' +version = '4.4' # CHANGEME # The full version, including alpha/beta/rc tags. -release = '4.2' +release = '4.4b1' # CHANGEME # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/config.rst b/doc/config.rst index afa6757..0740ef1 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -36,10 +36,11 @@ rather than put in your home directory. A different name for the configuration file can be specified with the ``--rcfile=FILE`` command line option. -Coverage.py will read settings from a ``setup.cfg`` file if no other -configuration file is used. In this case, the section names have "coverage:" +Coverage.py will read settings from other usual configuration files if no other +configuration file is used. It will automatically read from "setup.cfg" or +"tox.ini" if they exist. In this case, the section names have "coverage:" prefixed, so the ``[run]`` options described below will be found in the -``[coverage:run]`` section of ``setup.cfg``. +``[coverage:run]`` section of the file. Syntax @@ -125,6 +126,12 @@ Before version 4.2, this option only accepted a single string. for storing or reporting coverage. This value can include a path to another directory. +.. _config_run_disable_warnings: + +``disable_warnings`` (multi-string): a list of warnings to disable. Warnings +that can be disabled include a short string at the end, the name of the +warning. See :ref:`cmd_warnings` for specific warnings. + ``debug`` (multi-string): a list of debug options. See :ref:`the run --debug option <cmd_run_debug>` for details. @@ -149,7 +156,8 @@ for more information. measure during execution. See :ref:`source` for details. ``timid`` (boolean, default False): use a simpler but slower trace method. -Try this if you get seemingly impossible results. +This uses PyTracer instead of CTracer, and is only needed in very unusual +circumstances. Try this if you get seemingly impossible results. .. _config_paths: @@ -167,7 +175,7 @@ equivalent when combining data from different machines:: c:\myproj\src The names of the entries are ignored, you may choose any name that you like. -The value is a lists of strings. When combining data with the ``combine`` +The value is a list of strings. When combining data with the ``combine`` command, two file paths will be combined if they start with paths from the same list. diff --git a/doc/contributing.rst b/doc/contributing.rst index f631dd2..ba1863f 100644 --- a/doc/contributing.rst +++ b/doc/contributing.rst @@ -42,6 +42,9 @@ these steps: #. (Optional, but recommended) Create a virtualenv to work in, and activate it. +.. like this: + mkvirtualenv -p /usr/local/pythonz/pythons/CPython-2.7.11/bin/python coverage + #. Clone the repo:: $ hg clone https://bitbucket.org/ned/coveragepy @@ -64,51 +67,49 @@ The tests are written as standard unittest-style tests, and are run with `tox`_:: $ tox - py27 create: /Users/ned/coverage/trunk/.tox/py27 - py27 installdeps: nose==1.3.7, mock==1.3.0, PyContracts==1.7.6, gevent==1.0.2, eventlet==0.17.4, greenlet==0.4.7 - py27 develop-inst: /Users/ned/coverage/trunk - py27 installed: -f /Users/ned/Downloads/local_pypi,-e hg+ssh://hg@bitbucket.org/ned/coveragepy@22fe9a2b7796f6498aa013c860c268ac21651226#egg=coverage-dev,decorator==4.0.2,eventlet==0.17.4,funcsigs==0.4,gevent==1.0.2,greenlet==0.4.7,mock==1.3.0,nose==1.3.7,pbr==1.6.0,PyContracts==1.7.6,pyparsing==2.0.3,six==1.9.0,wheel==0.24.0 - py27 runtests: PYTHONHASHSEED='1294330776' + py27 develop-inst-noop: /Users/ned/coverage/trunk + py27 installed: apipkg==1.4,-e hg+ssh://hg@bitbucket.org/ned/coveragepy@6664140e34beddd6fee99b729bb9f4545a429c12#egg=coverage,covtestegg1==0.0.0,decorator==4.0.10,eventlet==0.19.0,execnet==1.4.1,funcsigs==1.0.2,gevent==1.1.2,greenlet==0.4.10,mock==2.0.0,pbr==1.10.0,py==1.4.31,PyContracts==1.7.12,pyparsing==2.1.10,pytest==3.0.5.dev0,pytest-warnings==0.2.0,pytest-xdist==1.15.0,six==1.10.0,unittest-mixins==1.1.1 + py27 runtests: PYTHONHASHSEED='4113423111' py27 runtests: commands[0] | python setup.py --quiet clean develop + no previously-included directories found matching 'tests/eggsrc/dist' + no previously-included directories found matching 'tests/eggsrc/*.egg-info' py27 runtests: commands[1] | python igor.py zip_mods install_egg remove_extension py27 runtests: commands[2] | python igor.py test_with_tracer py - === CPython 2.7.10 with Python tracer (.tox/py27/bin/python) === - ............................................................................(etc) - ---------------------------------------------------------------------- - Ran 592 tests in 65.524s - - OK (SKIP=20) + === CPython 2.7.12 with Python tracer (.tox/py27/bin/python) === + gw0 [679] / gw1 [679] / gw2 [679] + scheduling tests via LoadScheduling + ...........ss...................................................................................ss...s.......s...........................s...............................................................................s.....................................................................................................................................................s.........................................................................................s.s.s.s.s.ssssssssssss.ss..................................................s...................................................................s.............................................................................. + 649 passed, 30 skipped in 42.89 seconds py27 runtests: commands[3] | python setup.py --quiet build_ext --inplace py27 runtests: commands[4] | python igor.py test_with_tracer c - === CPython 2.7.10 with C tracer (.tox/py27/bin/python) === - ............................................................................(etc) - ---------------------------------------------------------------------- - Ran 592 tests in 69.635s - - OK (SKIP=4) - py33 create: /Users/ned/coverage/trunk/.tox/py33 - py33 installdeps: nose==1.3.7, mock==1.3.0, PyContracts==1.7.6, greenlet==0.4.7 - py33 develop-inst: /Users/ned/coverage/trunk - py33 installed: -f /Users/ned/Downloads/local_pypi,-e hg+ssh://hg@bitbucket.org/ned/coveragepy@22fe9a2b7796f6498aa013c860c268ac21651226#egg=coverage-dev,decorator==4.0.2,greenlet==0.4.7,mock==1.3.0,nose==1.3.7,pbr==1.6.0,PyContracts==1.7.6,pyparsing==2.0.3,six==1.9.0,wheel==0.24.0 - py33 runtests: PYTHONHASHSEED='1294330776' - py33 runtests: commands[0] | python setup.py --quiet clean develop - py33 runtests: commands[1] | python igor.py zip_mods install_egg remove_extension - py33 runtests: commands[2] | python igor.py test_with_tracer py - === CPython 3.3.6 with Python tracer (.tox/py33/bin/python) === - ............................................S...............................(etc) - ---------------------------------------------------------------------- - Ran 592 tests in 73.007s - - OK (SKIP=22) - py33 runtests: commands[3] | python setup.py --quiet build_ext --inplace - py33 runtests: commands[4] | python igor.py test_with_tracer c - === CPython 3.3.6 with C tracer (.tox/py33/bin/python) === - ............................................S...............................(etc) - ---------------------------------------------------------------------- - Ran 592 tests in 72.071s - - OK (SKIP=5) - (and so on...) + === CPython 2.7.12 with C tracer (.tox/py27/bin/python) === + gw0 [679] / gw1 [679] / gw2 [679] + scheduling tests via LoadScheduling + ............ss................................................................................s..s.....s......s.........................s..........................................................................................s............................................................................................................s............................................................................................................................s...................................................................s........................................................................s............................................................................ + 667 passed, 12 skipped in 41.87 seconds + py35 develop-inst-noop: /Users/ned/coverage/trunk + py35 installed: apipkg==1.4,-e hg+ssh://hg@bitbucket.org/ned/coveragepy@6664140e34beddd6fee99b729bb9f4545a429c12#egg=coverage,covtestegg1==0.0.0,decorator==4.0.10,eventlet==0.19.0,execnet==1.4.1,gevent==1.1.2,greenlet==0.4.10,mock==2.0.0,pbr==1.10.0,py==1.4.31,PyContracts==1.7.12,pyparsing==2.1.10,pytest==3.0.5.dev0,pytest-warnings==0.2.0,pytest-xdist==1.15.0,six==1.10.0,unittest-mixins==1.1.1 + py35 runtests: PYTHONHASHSEED='4113423111' + py35 runtests: commands[0] | python setup.py --quiet clean develop + no previously-included directories found matching 'tests/eggsrc/dist' + no previously-included directories found matching 'tests/eggsrc/*.egg-info' + py35 runtests: commands[1] | python igor.py zip_mods install_egg remove_extension + py35 runtests: commands[2] | python igor.py test_with_tracer py + === CPython 3.5.2 with Python tracer (.tox/py35/bin/python) === + gw0 [679] / gw1 [679] / gw2 [679] + scheduling tests via LoadScheduling + ............s..........................................................................................................................................................s..s...........................................................................................................................................................................................s.................................................................................................sssssssssssssssssss............................................................s................................................................s.............................................................................. + 654 passed, 25 skipped in 47.25 seconds + py35 runtests: commands[3] | python setup.py --quiet build_ext --inplace + py35 runtests: commands[4] | python igor.py test_with_tracer c + === CPython 3.5.2 with C tracer (.tox/py35/bin/python) === + gw0 [679] / gw1 [679] / gw2 [679] + scheduling tests via LoadScheduling + ...........s...............................................................................................................................................................................................s......s..........................................................................................................................................................s.................................................................................................s....................................................................................................................................s.................................................................................. + 673 passed, 6 skipped in 53.20 seconds + _________________________________________________________________________________________ summary __________________________________________________________________________________________ + py27: commands succeeded + py35: commands succeeded Tox runs the complete test suite twice for each version of Python you have installed. The first run uses the Python implementation of the trace function, @@ -118,16 +119,20 @@ To limit tox to just a few versions of Python, use the ``-e`` switch:: $ tox -e py27,py33 -To run just a few tests, you can use nose test selector syntax:: +To run just a few tests, you can use `pytest test selectors`_:: - $ tox tests.test_misc:SetupPyTest.test_metadata + $ tox tests/test_misc.py + $ tox tests/test_misc.py::SetupPyTest + $ tox tests/test_misc.py::SetupPyTest::test_metadata -This looks in `tests/test_misc.py` to find the `SetupPyTest` class, and runs -the `test_metadata` test method. +These command run the tests in one file, one class, and just one test, +respectively. Of course, run all the tests on every version of Python you have, before submitting a change. +.. _pytest test selectors: http://doc.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests + Lint, etc --------- @@ -144,7 +149,12 @@ The source is pylint-clean, even if it's because there are pragmas quieting some warnings. Please try to keep it that way, but don't let pylint warnings keep you from sending patches. I can clean them up. -Lines should be kept to a 100-character maximum length. +Lines should be kept to a 100-character maximum length. I recommend an +`editorconfig.org`_ plugin for your editor of choice. + +Other style questions are best answered by looking at the existing code. +Formatting of docstrings, comments, long lines, and so on, should match the +code that already exists. Coverage testing coverage.py @@ -165,8 +175,9 @@ Contributing When you are ready to contribute a change, any way you can get it to me is probably fine. A pull request on Bitbucket is great, but a simple diff or -patch is great too. +patch works too. +.. _editorconfig.org: http://editorconfig.org .. _Mercurial: https://www.mercurial-scm.org/ .. _tox: http://tox.testrun.org/ diff --git a/doc/faq.rst b/doc/faq.rst index 6609ab3..ea69aea 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -7,18 +7,46 @@ FAQ and other help ================== -.. :history: 20090613T141800, brand new docs. -.. :history: 20091005T073900, updated for 3.1. -.. :history: 20091127T201500, updated for 3.2. -.. :history: 20110605T175500, add the announcement mailing list. -.. :history: 20121231T104700, Tweak the py3 text. - Frequently asked questions -------------------------- -**Q: I use nose to run my tests, and its cover plugin doesn't let me create -HTML or XML reports. What should I do?** +**Q: How do I use coverage.py with nose?** + +The best way to use nose and coverage.py together is to run nose under +coverage.py:: + + coverage run $(which nosetests) + +You can also use ``nosetests --with-coverage`` to use `nose's built-in +plugin`__, but it isn't recommended. + +The nose plugin doesn't expose all the functionality and configurability of +coverage.py, and it uses different command-line options from those described in +coverage.py's documentation. Additionally nose and its coverage plugin are +unmaintained at this point, so they aren't receiving any fixes or other +updates. + +__ http://nose.readthedocs.io/en/latest/plugins/cover.html + + +**Q: How do I run nosetests under coverage.py with tox?** + +Assuming you've installed tox in a virtualenv, you can do this in tox.ini:: + + [testenv] + commands = coverage run {envbindir}/nosetests + +Coverage.py needs a path to the nosetests executable, but ``coverage run +$(which nosetests)`` doesn't work in tox.ini because tox doesn't handle the +shell command substitution. Tox's `string substitution`__ shown above does the +trick. + +__ http://tox.readthedocs.io/en/latest/config.html#substitutions + + +**Q: I use nose to run my tests, and its coverage plugin doesn't let me create +HTML or XML reports. What should I do?** First run your tests and collect coverage data with `nose`_ and its plugin. This will write coverage data into a .coverage file. Then run coverage.py from @@ -123,4 +151,4 @@ Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of .. _Gareth Rees: http://garethrees.org/ .. _Ned Batchelder: http://nedbatchelder.com -.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/AUTHORS.txt +.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/CONTRIBUTORS.txt diff --git a/doc/index.rst b/doc/index.rst index efbb556..1b438c3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -48,6 +48,8 @@ Coverage.py .. :history: 20160510T125300, updated for 4.1b3 .. :history: 20160521T074500, updated for 4.1 .. :history: 20160726T161300, updated for 4.2 +.. :history: 20161226T160400, updated for 4.3 +.. :history: 20170116T180100, updated for 4.3.2 Coverage.py is a tool for measuring code coverage of Python programs. It @@ -60,28 +62,30 @@ not. .. ifconfig:: not prerelease - The latest version is coverage.py 4.2, released July 26th 2016. It + The latest version is coverage.py 4.3.4, released January 17th 2017. It is supported on: * Python versions 2.6, 2.7, 3.3, 3.4, 3.5, and 3.6. - * PyPy 4.0 and 5.1. + * PyPy2 5.6 and PyPy3 5.5. - * PyPy3 2.4 and 5.2 + * Jython 2.7.1, though only for running code, not reporting. .. ifconfig:: prerelease - The latest version is coverage.py 4.2b1, released July 4th 2016. It is + The latest version is coverage.py 4.4b1, released April 4th 2017. It is supported on: - * Python versions 2.6, 2.7, 3.3, 3.4, and 3.5. + * Python versions 2.6, 2.7, 3.3, 3.4, 3.5, and 3.6. + + * PyPy2 5.6 and PyPy3 5.5. - * PyPy 4.0 and 5.1. + * Jython 2.7.1, though only for running code, not reporting. - * PyPy3 2.4 and 5.2. + * IronPython 2.7.7, though only for running code, not reporting. **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 4.1, `described here`_. + apply.** The latest stable version is coverage.py 4.3.4, `described here`_. .. _described here: http://nedbatchelder.com/code/coverage @@ -145,14 +149,15 @@ If you need more control over how your project is measured, you can use the :ref:`API <api>`. Some test runners provide coverage integration to make it easy to use -coverage.py while running tests. For example, `nose`_ has a `cover plug-in`_. +coverage.py while running tests. For example, `pytest`_ has the `pytest-cov`_ +plugin. You can fine-tune coverage.py's view of your code by directing it to ignore parts that you know aren't interesting. See :ref:`source` and :ref:`excluding` for details. -.. _nose: http://nose.readthedocs.io -.. _cover plug-in: https://nose.readthedocs.io/en/latest/plugins/cover.html +.. _pytest: http://doc.pytest.org +.. _pytest-cov: http://pytest-cov.readthedocs.io .. _contact: @@ -176,7 +181,7 @@ GitHub. `I can be reached`_ in a number of ways. I'm happy to answer questions about using coverage.py. -.. _I can be reached: http://nedbatchelder.com/site/aboutned.html +.. _I can be reached: http://nedbatchelder.com/site/aboutned.html diff --git a/doc/install.rst b/doc/install.rst index bcea93f..5774d1b 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -24,17 +24,16 @@ Installation .. :history: 20131005T210600, updated for 3.7. .. :history: 20131212T213500, updated for 3.7.1. .. :history: 20140927T102700, updated for 4.0a1. +.. :history: 20161218T173000, remove alternate instructions w/ Distribute .. highlight:: console .. _coverage_pypi: http://pypi.python.org/pypi/coverage .. _setuptools: http://pypi.python.org/pypi/setuptools -.. _Distribute: http://packages.python.org/distribute/ -Installing coverage.py is done in the usual ways. The simplest way is with -pip:: +You can install coverage.py in the usual ways. The simplest way is with pip:: $ pip install coverage @@ -45,18 +44,6 @@ pip:: $ pip install --pre coverage -The alternate old-school technique is: - -#. Install (or already have installed) `setuptools`_ or `Distribute`_. - -#. Download the appropriate kit from the - `coverage.py page on the Python Package Index`__. - -#. Run ``python setup.py install``. - -.. __: coverage_pypi_ - - .. _install_extension: C Extension diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index 177fca2..94402b8 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -137,6 +137,9 @@ COMMAND REFERENCE \-i, --ignore-errors Ignore errors while reading source files. + \--skip-covered + Skip files with 100% coverage. + \--title `TITLE` Use the text string `TITLE` as the title on the HTML. @@ -223,7 +226,7 @@ The |command| command is a Python program which calls the ``coverage`` Python library to do all the work. The library was originally developed by Gareth Rees, and is now developed -by Ned Batchelder. +by Ned Batchelder and many others. This manual page was written by |author|. diff --git a/doc/requirements.pip b/doc/requirements.pip index ff2a9d3..325d4f2 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -2,10 +2,10 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ -pyenchant==1.6.7 -sphinx==1.4.5 -sphinxcontrib-spelling==2.2.0 -sphinx_rtd_theme==0.1.9 +pyenchant==1.6.8 +sphinx==1.5.5 +sphinxcontrib-spelling==2.3.0 +sphinx_rtd_theme==0.2.4 # A version of doc8 with a -q flag. git+https://github.com/nedbat/doc8.git#egg=doc8==0.0 diff --git a/doc/sample_html/cogapp___init___py.html b/doc/sample_html/cogapp___init___py.html index 0776b09..a435385 100644 --- a/doc/sample_html/cogapp___init___py.html +++ b/doc/sample_html/cogapp___init___py.html @@ -99,8 +99,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp___main___py.html b/doc/sample_html/cogapp___main___py.html index 79ba892..7a1c066 100644 --- a/doc/sample_html/cogapp___main___py.html +++ b/doc/sample_html/cogapp___main___py.html @@ -91,8 +91,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_backward_py.html b/doc/sample_html/cogapp_backward_py.html index 64a65cb..0a98934 100644 --- a/doc/sample_html/cogapp_backward_py.html +++ b/doc/sample_html/cogapp_backward_py.html @@ -133,8 +133,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_cogapp_py.html b/doc/sample_html/cogapp_cogapp_py.html index dd3ccdc..3d730b9 100644 --- a/doc/sample_html/cogapp_cogapp_py.html +++ b/doc/sample_html/cogapp_cogapp_py.html @@ -849,6 +849,7 @@ <p id="n778" class="pln"><a href="#n778">778</a></p> <p id="n779" class="pln"><a href="#n779">779</a></p> <p id="n780" class="pln"><a href="#n780">780</a></p> +<p id="n781" class="pln"><a href="#n781">781</a></p> </td> <td class="text"> @@ -866,7 +867,7 @@ <p id="t12" class="pln"><span class="strut"> </span></p> <p id="t13" class="stm run hide_run"><span class="nam">__all__</span> <span class="op">=</span> <span class="op">[</span><span class="str">'Cog'</span><span class="op">,</span> <span class="str">'CogUsageError'</span><span class="op">]</span><span class="strut"> </span></p> <p id="t14" class="pln"><span class="strut"> </span></p> -<p id="t15" class="stm run hide_run"><span class="nam">__version__</span> <span class="op">=</span> <span class="str">'2.5'</span> <span class="com"># History at the end of the file.</span><span class="strut"> </span></p> +<p id="t15" class="stm run hide_run"><span class="nam">__version__</span> <span class="op">=</span> <span class="str">'2.5.1'</span> <span class="com"># History at the end of the file.</span><span class="strut"> </span></p> <p id="t16" class="pln"><span class="strut"> </span></p> <p id="t17" class="stm run hide_run"><span class="nam">usage</span> <span class="op">=</span> <span class="str">"""\</span><span class="strut"> </span></p> <p id="t18" class="pln"><span class="str">cog - generate code with inlined Python code.</span><span class="strut"> </span></p> @@ -1632,6 +1633,7 @@ <p id="t778" class="pln"><span class="com"># 20150107: Added --verbose to control what files get listed.</span><span class="strut"> </span></p> <p id="t779" class="pln"><span class="com"># 20150111: Version 2.4</span><span class="strut"> </span></p> <p id="t780" class="pln"><span class="com"># 20160213: v2.5: -o makes needed directories, thanks Jean-François Giraud.</span><span class="strut"> </span></p> +<p id="t781" class="pln"><span class="com"># 20161019: Added a LICENSE.txt file.</span><span class="strut"> </span></p> </td> </tr> @@ -1641,8 +1643,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_makefiles_py.html b/doc/sample_html/cogapp_makefiles_py.html index 8caa21a..aab49aa 100644 --- a/doc/sample_html/cogapp_makefiles_py.html +++ b/doc/sample_html/cogapp_makefiles_py.html @@ -207,8 +207,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_test_cogapp_py.html b/doc/sample_html/cogapp_test_cogapp_py.html index c50de2a..f3d49f9 100644 --- a/doc/sample_html/cogapp_test_cogapp_py.html +++ b/doc/sample_html/cogapp_test_cogapp_py.html @@ -4525,8 +4525,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_test_makefiles_py.html b/doc/sample_html/cogapp_test_makefiles_py.html index 9fb0ac5..7d7364d 100644 --- a/doc/sample_html/cogapp_test_makefiles_py.html +++ b/doc/sample_html/cogapp_test_makefiles_py.html @@ -265,8 +265,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_test_whiteutils_py.html b/doc/sample_html/cogapp_test_whiteutils_py.html index 213e576..3d67e72 100644 --- a/doc/sample_html/cogapp_test_whiteutils_py.html +++ b/doc/sample_html/cogapp_test_whiteutils_py.html @@ -285,8 +285,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/cogapp_whiteutils_py.html b/doc/sample_html/cogapp_whiteutils_py.html index a6bb174..1a44dc2 100644 --- a/doc/sample_html/cogapp_whiteutils_py.html +++ b/doc/sample_html/cogapp_whiteutils_py.html @@ -223,8 +223,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="index.html">« index</a> <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html index 64fee15..8200489 100644 --- a/doc/sample_html/index.html +++ b/doc/sample_html/index.html @@ -9,7 +9,7 @@ <link rel="stylesheet" href="style.css" type="text/css"> <script type="text/javascript" src="jquery.min.js"></script> - <script type="text/javascript" src="jquery.debounce.min.js"></script> + <script type="text/javascript" src="jquery.ba-throttle-debounce.min.js"></script> <script type="text/javascript" src="jquery.tablesorter.min.js"></script> <script type="text/javascript" src="jquery.hotkeys.js"></script> <script type="text/javascript" src="coverage_html.js"></script> @@ -202,8 +202,8 @@ <div id="footer"> <div class="content"> <p> - <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.2</a>, - created at 2016-07-26 17:01 + <a class="nav" href="https://coverage.readthedocs.io">coverage.py v4.3.2</a>, + created at 2017-01-16 18:24 </p> </div> </div> diff --git a/doc/sample_html/jquery.debounce.min.js b/doc/sample_html/jquery.ba-throttle-debounce.min.js index 648fe5d..648fe5d 100644 --- a/doc/sample_html/jquery.debounce.min.js +++ b/doc/sample_html/jquery.ba-throttle-debounce.min.js diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json index 2cbfd1d..03bc1bc 100644 --- a/doc/sample_html/status.json +++ b/doc/sample_html/status.json @@ -1 +1 @@ -{"files": {"cogapp_test_whiteutils_py": {"index": {"relative_filename": "cogapp/test_whiteutils.py", "html_filename": "cogapp_test_whiteutils_py.html", "nums": [1, 69, 0, 69, 0, 0, 0]}, "hash": "f7a3c04788858b652fcfb29825e1e8d4"}, "cogapp_test_makefiles_py": {"index": {"relative_filename": "cogapp/test_makefiles.py", "html_filename": "cogapp_test_makefiles_py.html", "nums": [1, 55, 0, 55, 6, 0, 6]}, "hash": "ff1f44c04d08ae202f5164e1ba75818e"}, "cogapp_cogapp_py": {"index": {"relative_filename": "cogapp/cogapp.py", "html_filename": "cogapp_cogapp_py.html", "nums": [1, 427, 4, 197, 176, 26, 122]}, "hash": "71af1837c21b50aa97bd6a65245dcfb5"}, "cogapp___init___py": {"index": {"relative_filename": "cogapp/__init__.py", "html_filename": "cogapp___init___py.html", "nums": [1, 2, 0, 0, 0, 0, 0]}, "hash": "589b4cc38603d62593ba92f20950eb8a"}, "cogapp_backward_py": {"index": {"relative_filename": "cogapp/backward.py", "html_filename": "cogapp_backward_py.html", "nums": [1, 19, 0, 8, 2, 1, 1]}, "hash": "5b76f23e07605fde0795af44bf5eeedf"}, "cogapp_test_cogapp_py": {"index": {"relative_filename": "cogapp/test_cogapp.py", "html_filename": "cogapp_test_cogapp_py.html", "nums": [1, 704, 6, 486, 6, 0, 4]}, "hash": "43b3b366e5f19ac62d84f20d2e39553a"}, "cogapp_makefiles_py": {"index": {"relative_filename": "cogapp/makefiles.py", "html_filename": "cogapp_makefiles_py.html", "nums": [1, 28, 3, 20, 14, 0, 14]}, "hash": "8e1d1a527f997b8217a7853660c0d6ab"}, "cogapp_whiteutils_py": {"index": {"relative_filename": "cogapp/whiteutils.py", "html_filename": "cogapp_whiteutils_py.html", "nums": [1, 45, 0, 3, 32, 3, 3]}, "hash": "9ca932a2b79ba4730c48196ed24b6cb9"}, "cogapp___main___py": {"index": {"relative_filename": "cogapp/__main__.py", "html_filename": "cogapp___main___py.html", "nums": [1, 3, 0, 3, 0, 0, 0]}, "hash": "c846304fff9f9b5f7510a86b60c3c3c6"}}, "version": "4.2", "settings": "92cfe81b6752194d8c834dfd98ed7d30", "format": 1}
\ No newline at end of file +{"files":{"cogapp_test_whiteutils_py":{"index":{"relative_filename":"cogapp/test_whiteutils.py","html_filename":"cogapp_test_whiteutils_py.html","nums":[1,69,0,69,0,0,0]},"hash":"f7a3c04788858b652fcfb29825e1e8d4"},"cogapp_test_makefiles_py":{"index":{"relative_filename":"cogapp/test_makefiles.py","html_filename":"cogapp_test_makefiles_py.html","nums":[1,55,0,55,6,0,6]},"hash":"ff1f44c04d08ae202f5164e1ba75818e"},"cogapp_cogapp_py":{"index":{"relative_filename":"cogapp/cogapp.py","html_filename":"cogapp_cogapp_py.html","nums":[1,427,4,197,176,26,122]},"hash":"14e09b853b264fdef21aef70c12a3e33"},"cogapp___init___py":{"index":{"relative_filename":"cogapp/__init__.py","html_filename":"cogapp___init___py.html","nums":[1,2,0,0,0,0,0]},"hash":"589b4cc38603d62593ba92f20950eb8a"},"cogapp_backward_py":{"index":{"relative_filename":"cogapp/backward.py","html_filename":"cogapp_backward_py.html","nums":[1,19,0,8,2,1,1]},"hash":"5b76f23e07605fde0795af44bf5eeedf"},"cogapp_test_cogapp_py":{"index":{"relative_filename":"cogapp/test_cogapp.py","html_filename":"cogapp_test_cogapp_py.html","nums":[1,704,6,486,6,0,4]},"hash":"43b3b366e5f19ac62d84f20d2e39553a"},"cogapp_makefiles_py":{"index":{"relative_filename":"cogapp/makefiles.py","html_filename":"cogapp_makefiles_py.html","nums":[1,28,3,20,14,0,14]},"hash":"8e1d1a527f997b8217a7853660c0d6ab"},"cogapp_whiteutils_py":{"index":{"relative_filename":"cogapp/whiteutils.py","html_filename":"cogapp_whiteutils_py.html","nums":[1,45,0,3,32,3,3]},"hash":"9ca932a2b79ba4730c48196ed24b6cb9"},"cogapp___main___py":{"index":{"relative_filename":"cogapp/__main__.py","html_filename":"cogapp___main___py.html","nums":[1,3,0,3,0,0,0]},"hash":"c846304fff9f9b5f7510a86b60c3c3c6"}},"version":"4.3.2","settings":"ab77d6eb8ebccdfdc6ebe1a73a96a083","format":1}
\ No newline at end of file diff --git a/doc/source.rst b/doc/source.rst index 8f5b31b..8d831c4 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -35,9 +35,10 @@ source inside these directories or packages will be measured. Specifying the source option also enables coverage.py to report on unexecuted files, since it can search the source tree for files that haven't been measured at all. Only importable files (ones at the root of the tree, or in directories with a -``__init__.py`` file) will be considered, and files with unusual punctuation in +``__init__.py`` file) will be considered. Files with unusual punctuation in their names will be skipped (they are assumed to be scratch files written by -text editors). +text editors). Files that do not end with ``.py`` or ``.pyo`` or ``.pyc`` +will also be skipped. You can further fine-tune coverage.py's attention with the ``--include`` and ``--omit`` switches (or ``[run] include`` and ``[run] omit`` configuration @@ -1,9 +1,3 @@ -* Making a working tree - -mkvirtualenv -p /usr/local/pythonz/pythons/CPython-2.7.11/bin/python coverage -pip install -U pip -pip install -r requirements/dev.pip - * Release checklist - Version number in coverage/version.py @@ -14,18 +8,22 @@ pip install -r requirements/dev.pip - Python version number in classifiers in setup.py - Copyright date in NOTICE.txt - Update CHANGES.rst, including release date. -- Update README.rst, including "New in x.y:" +- Update README.rst + - "New in x.y:" + - version number in the commits-since badge - Update docs - - Version, date, and changes in doc/changes.rst - Version, date and python versions in doc/index.rst - Version and copyright date in doc/conf.py + - Look for CHANGEME comments - Don't forget the man page: doc/python-coverage.1.txt - Check that the docs build correctly: $ tox -e doc - Done with changes to source files, check them in. - hg push - Generate new sample_html to get the latest, incl footer version number: + make clean pip install -e . + pip install nose cd ~/cog/trunk rm -rf htmlcov coverage run --branch --source=cogapp -m nose cogapp/test_cogapp.py:CogTestsInMemory @@ -34,30 +32,33 @@ pip install -r requirements/dev.pip - IF BETA: rm -f ~/coverage/trunk/doc/sample_html_beta/*.* cp -r htmlcov/ ~/coverage/trunk/doc/sample_html_beta/ - - ELSE: + - IF NOT BETA: rm -f ~/coverage/trunk/doc/sample_html/*.* cp -r htmlcov/ ~/coverage/trunk/doc/sample_html/ cd ~/coverage/trunk - check in the new sample html - - IF BETA: - - Build and publish docs: + - IF NOT BETA: + check in the new sample html + - Build and publish docs: + - IF BETA: $ make publishbeta - - ELSE: - - Build and publish docs: + - ELSE: $ make publish - Kits: + - Start fresh: + - $ make clean - Source kit and wheels: - - $ make clean kit wheel + - $ make kit wheel + - Linux wheels: + - $ make manylinux - Windows kits - wait for over an hour for Appveyor to build kits. - https://ci.appveyor.com/project/nedbat/coveragepy - $ make download_appveyor - examine the dist directory, and remove anything that looks malformed. - Update PyPi: - - $ make pypi - upload kits: - $ make kit_upload - - Visit http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=coverage : + - Visit http://pypi.python.org/pypi?:action=pkg_edit&name=coverage : - show/hide the proper versions. - Tag the tree - hg tag -m "Coverage 3.0.1" coverage-3.0.1 @@ -67,7 +68,7 @@ pip install -r requirements/dev.pip - visit https://readthedocs.org/projects/coverage/versions/ - find the latest tag in the inactive list, edit it, make it active. - IF NOT BETA: - - visit https://readthedocs.org/dashboard/coverage/advanced/ + - visit https://readthedocs.org/dashboard/coverage/versions/ - change the default version to the new version - Update bitbucket: - Issue tracker should get new version number in picker. @@ -76,12 +77,13 @@ pip install -r requirements/dev.pip - Announce on coveragepy-announce@googlegroups.com . - Announce on TIP. - -* Building - -- Create PythonXX\Lib\distutils\distutils.cfg:: - [build] - compiler = mingw32 +- Bump version: + - coverage/version.py + - increment version number + - IF NOT BETA: + - set to alpha-0 if just released + - CHANGES.rst + - add an "Unreleased" section to the top. * Testing @@ -20,11 +20,22 @@ import textwrap import warnings import zipfile +import pytest # We want to see all warnings while we are running tests. But we also need to # disable warnings for some of the more complex setting up of tests. warnings.simplefilter("default") +# Silence specific warnings that are not our fault. +warnings.filterwarnings("ignore", module="xdist", message="type argument to addoption") +warnings.filterwarnings("ignore", module="flaky", message="type argument to addoption") +warnings.filterwarnings( + # https://github.com/pytest-dev/pytest/issues/2118 + "ignore", + module="_pytest", + message="This usage is deprecated, please use pytest.* instead" +) + @contextlib.contextmanager def ignore_warnings(): @@ -79,10 +90,7 @@ def should_skip(tracer): if tracer == "py": skipper = os.environ.get("COVERAGE_NO_PYTRACER") else: - skipper = ( - os.environ.get("COVERAGE_NO_EXTENSION") or - os.environ.get("COVERAGE_NO_CTRACER") - ) + skipper = os.environ.get("COVERAGE_NO_CTRACER") if skipper: msg = "Skipping tests " + label_for_tracer(tracer) @@ -94,21 +102,16 @@ def should_skip(tracer): return msg -def run_tests(tracer, *nose_args): +def run_tests(tracer, *runner_args): """The actual running of tests.""" - with ignore_warnings(): - import nose.core - if 'COVERAGE_TESTING' not in os.environ: os.environ['COVERAGE_TESTING'] = "True" print_banner(label_for_tracer(tracer)) - nose_args = ["nosetests"] + list(nose_args) - nose.core.main(argv=nose_args) + return pytest.main(list(runner_args)) -def run_tests_with_coverage(tracer, *nose_args): +def run_tests_with_coverage(tracer, *runner_args): """Run tests, but with coverage.""" - # Need to define this early enough that the first import of env.py sees it. os.environ['COVERAGE_TESTING'] = "True" os.environ['COVERAGE_PROCESS_START'] = os.path.abspath('metacov.ini') @@ -117,8 +120,7 @@ def run_tests_with_coverage(tracer, *nose_args): # Create the .pth file that will let us measure coverage in sub-processes. # The .pth file seems to have to be alphabetically after easy-install.pth # or the sys.path entries aren't created right? - import nose - pth_dir = os.path.dirname(os.path.dirname(nose.__file__)) + pth_dir = os.path.dirname(pytest.__file__) pth_path = os.path.join(pth_dir, "zzz_metacov.pth") with open(pth_path, "w") as pth_file: pth_file.write("import coverage; coverage.process_startup()\n") @@ -157,12 +159,9 @@ def run_tests_with_coverage(tracer, *nose_args): import coverage # pylint: disable=reimported sys.modules.update(covmods) - # Run nosetests, with the arguments from our command line. - try: - run_tests(tracer, *nose_args) - except SystemExit: - # nose3 seems to raise SystemExit, not sure why? - pass + # Run tests, with the arguments from our command line. + status = run_tests(tracer, *runner_args) + finally: cov.stop() os.remove(pth_path) @@ -170,6 +169,8 @@ def run_tests_with_coverage(tracer, *nose_args): cov.combine() cov.save() + return status + def do_combine_html(): """Combine data from a meta-coverage run, and make the HTML and XML reports.""" @@ -184,8 +185,8 @@ def do_combine_html(): cov.xml_report() -def do_test_with_tracer(tracer, *noseargs): - """Run nosetests with a particular tracer.""" +def do_test_with_tracer(tracer, *runner_args): + """Run tests with a particular tracer.""" # If we should skip these tests, skip them. skip_msg = should_skip(tracer) if skip_msg: @@ -194,9 +195,9 @@ def do_test_with_tracer(tracer, *noseargs): os.environ["COVERAGE_TEST_TRACER"] = tracer if os.environ.get("COVERAGE_COVERAGE", ""): - return run_tests_with_coverage(tracer, *noseargs) + return run_tests_with_coverage(tracer, *runner_args) else: - return run_tests(tracer, *noseargs) + return run_tests(tracer, *runner_args) def do_zip_mods(): @@ -269,13 +270,13 @@ def do_check_eol(): with open(fname, "rb") as f: for n, line in enumerate(f, start=1): if crlf: - if "\r" in line: + if b"\r" in line: print("%s@%d: CR found" % (fname, n)) return if trail_white: line = line[:-1] if not crlf: - line = line.rstrip('\r') + line = line.rstrip(b'\r') if line.rstrip() != line: print("%s@%d: trailing whitespace found" % (fname, n)) return @@ -285,9 +286,9 @@ def do_check_eol(): def check_files(root, patterns, **kwargs): """Check a number of files for whitespace abuse.""" - for root, dirs, files in os.walk(root): + for where, dirs, files in os.walk(root): for f in files: - fname = os.path.join(root, f) + fname = os.path.join(where, f) for p in patterns: if fnmatch.fnmatch(fname, p): check_file(fname, **kwargs) diff --git a/lab/coverage-04.dtd b/lab/coverage-04.dtd new file mode 100644 index 0000000..e5a21bb --- /dev/null +++ b/lab/coverage-04.dtd @@ -0,0 +1,60 @@ +<!-- Portions (C) International Organization for Standardization 1986: + Permission to copy in any form is granted for use with + conforming SGML systems and applications as defined in + ISO 8879, provided this notice is included in all copies. +--> + +<!ELEMENT coverage (sources?,packages)> +<!ATTLIST coverage line-rate CDATA #REQUIRED> +<!ATTLIST coverage branch-rate CDATA #REQUIRED> +<!ATTLIST coverage lines-covered CDATA #REQUIRED> +<!ATTLIST coverage lines-valid CDATA #REQUIRED> +<!ATTLIST coverage branches-covered CDATA #REQUIRED> +<!ATTLIST coverage branches-valid CDATA #REQUIRED> +<!ATTLIST coverage complexity CDATA #REQUIRED> +<!ATTLIST coverage version CDATA #REQUIRED> +<!ATTLIST coverage timestamp CDATA #REQUIRED> + +<!ELEMENT sources (source*)> + +<!ELEMENT source (#PCDATA)> + +<!ELEMENT packages (package*)> + +<!ELEMENT package (classes)> +<!ATTLIST package name CDATA #REQUIRED> +<!ATTLIST package line-rate CDATA #REQUIRED> +<!ATTLIST package branch-rate CDATA #REQUIRED> +<!ATTLIST package complexity CDATA #REQUIRED> + +<!ELEMENT classes (class*)> + +<!ELEMENT class (methods,lines)> +<!ATTLIST class name CDATA #REQUIRED> +<!ATTLIST class filename CDATA #REQUIRED> +<!ATTLIST class line-rate CDATA #REQUIRED> +<!ATTLIST class branch-rate CDATA #REQUIRED> +<!ATTLIST class complexity CDATA #REQUIRED> + +<!ELEMENT methods (method*)> + +<!ELEMENT method (lines)> +<!ATTLIST method name CDATA #REQUIRED> +<!ATTLIST method signature CDATA #REQUIRED> +<!ATTLIST method line-rate CDATA #REQUIRED> +<!ATTLIST method branch-rate CDATA #REQUIRED> + +<!ELEMENT lines (line*)> + +<!ELEMENT line (conditions*)> +<!ATTLIST line number CDATA #REQUIRED> +<!ATTLIST line hits CDATA #REQUIRED> +<!ATTLIST line branch CDATA "false"> +<!ATTLIST line condition-coverage CDATA "100%"> + +<!ELEMENT conditions (condition*)> + +<!ELEMENT condition EMPTY> +<!ATTLIST condition number CDATA #REQUIRED> +<!ATTLIST condition type CDATA #REQUIRED> +<!ATTLIST condition coverage CDATA #REQUIRED> diff --git a/lab/show_pyc.py b/lab/show_pyc.py index 4eaa513..0a28e4f 100644 --- a/lab/show_pyc.py +++ b/lab/show_pyc.py @@ -17,6 +17,10 @@ def show_pyc_file(fname): modtime = time.asctime(time.localtime(struct.unpack('<L', moddate)[0])) print("magic %s" % (binascii.hexlify(magic))) print("moddate %s (%s)" % (binascii.hexlify(moddate), modtime)) + if sys.version_info >= (3, 3): + # 3.3 added another long to the header (size). + size = f.read(4) + print("pysize %s (%d)" % (binascii.hexlify(size), struct.unpack('<L', size)[0])) code = marshal.load(f) show_code(code) @@ -38,6 +42,7 @@ CO_FLAGS = [ ('CO_NOFREE', 0x00040), ('CO_COROUTINE', 0x00080), ('CO_ITERABLE_COROUTINE', 0x00100), + ('CO_ASYNC_GENERATOR', 0x00200), ('CO_GENERATOR_ALLOWED', 0x01000), ('CO_FUTURE_DIVISION', 0x02000), ('CO_FUTURE_ABSOLUTE_IMPORT', 0x04000), diff --git a/metacov.ini b/metacov.ini index c59ad81..55d0225 100644 --- a/metacov.ini +++ b/metacov.ini @@ -14,15 +14,40 @@ source = # We set a different pragma so our code won't be confused with test code. exclude_lines = pragma: not covered + + # Lines in test code that isn't covered: we are nested inside ourselves. pragma: nested + + # Lines that are only executed when we are debugging coverage.py. def __repr__ - raise AssertionError pragma: debugging + + # Lines that are only executed when we are not testing coverage.py. + pragma: not testing + + # Lines that we can't run during metacov. + pragma: no metacov + + # These lines only happen if tests fail. + raise AssertionError pragma: only failure + # OS error conditions that we can't (or don't care to) replicate. + pragma: cant happen + + # Jython needs special care. + pragma: only jython + skip.*Jython + + # IronPython isn't included in metacoverage. + pragma: only ironpython + partial_branches = pragma: part covered + pragma: if failure if env.TESTING: + if .* env.JYTHON + if .* env.IRONPYTHON ignore_errors = true precision = 1 diff --git a/lab/bug397.py b/perf/bug397.py index 4d72e90..4d72e90 100644 --- a/lab/bug397.py +++ b/perf/bug397.py diff --git a/perf/perf_measure.py b/perf/perf_measure.py new file mode 100644 index 0000000..3b0ae52 --- /dev/null +++ b/perf/perf_measure.py @@ -0,0 +1,188 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +# Run like this: +# .tox/py36/bin/python perf/perf_measure.py + +from collections import namedtuple +import os +import statistics +import sys +import tempfile +import time + +from unittest_mixins.mixins import make_file + +import coverage +from coverage.backward import import_local_file + +from tests.helpers import SuperModuleCleaner + + +class StressResult(namedtuple('StressResult', ['files', 'calls', 'lines', 'baseline', 'covered'])): + @property + def overhead(self): + return self.covered - self.baseline + + +TEST_FILE = """\ +def parent(call_count, line_count): + for _ in range(call_count): + child(line_count) + +def child(line_count): + for i in range(line_count): + x = 1 +""" + +def mk_main(file_count, call_count, line_count): + lines = [] + lines.extend( + "import test{}".format(idx) for idx in range(file_count) + ) + lines.extend( + "test{}.parent({}, {})".format(idx, call_count, line_count) for idx in range(file_count) + ) + return "\n".join(lines) + + +class StressTest(object): + + def __init__(self): + self.module_cleaner = SuperModuleCleaner() + + def _run_scenario(self, file_count, call_count, line_count): + self.module_cleaner.clean_local_file_imports() + + for idx in range(file_count): + make_file('test{}.py'.format(idx), TEST_FILE) + make_file('testmain.py', mk_main(file_count, call_count, line_count)) + + # Run it once just to get the disk caches loaded up. + import_local_file("testmain") + self.module_cleaner.clean_local_file_imports() + + # Run it to get the baseline time. + start = time.perf_counter() + import_local_file("testmain") + baseline = time.perf_counter() - start + self.module_cleaner.clean_local_file_imports() + + # Run it to get the covered time. + start = time.perf_counter() + cov = coverage.Coverage() + cov.start() + try: # pragma: nested + # Import the Python file, executing it. + import_local_file("testmain") + finally: # pragma: nested + # Stop coverage.py. + covered = time.perf_counter() - start + stats = cov.collector.tracers[0].get_stats() + if stats: + stats = stats.copy() + cov.stop() + + return baseline, covered, stats + + def _compute_overhead(self, file_count, call_count, line_count): + baseline, covered, stats = self._run_scenario(file_count, call_count, line_count) + + #print("baseline = {:.2f}, covered = {:.2f}".format(baseline, covered)) + # Empirically determined to produce the same numbers as the collected + # stats from get_stats(), with Python 3.6. + actual_file_count = 17 + file_count + actual_call_count = file_count * call_count + 156 * file_count + 85 + actual_line_count = ( + 2 * file_count * call_count * line_count + + 3 * file_count * call_count + + 769 * file_count + + 345 + ) + + if stats is not None: + assert actual_file_count == stats['files'] + assert actual_call_count == stats['calls'] + assert actual_line_count == stats['lines'] + print("File counts", file_count, actual_file_count, stats['files']) + print("Call counts", call_count, actual_call_count, stats['calls']) + print("Line counts", line_count, actual_line_count, stats['lines']) + print() + + return StressResult( + actual_file_count, + actual_call_count, + actual_line_count, + baseline, + covered, + ) + + fixed = 200 + numlo = 100 + numhi = 100 + step = 50 + runs = 5 + + def count_operations(self): + + def operations(thing): + for _ in range(self.runs): + for n in range(self.numlo, self.numhi+1, self.step): + kwargs = { + "file_count": self.fixed, + "call_count": self.fixed, + "line_count": self.fixed, + } + kwargs[thing+"_count"] = n + yield kwargs['file_count'] * kwargs['call_count'] * kwargs['line_count'] + + ops = sum(sum(operations(thing)) for thing in ["file", "call", "line"]) + print("{0:.1f}M operations".format(ops/1e6)) + + def check_coefficients(self): + # For checking the calculation of actual stats: + for f in range(1, 6): + for c in range(1, 6): + for l in range(1, 6): + _, _, stats = self._run_scenario(f, c, l) + print("{0},{1},{2},{3[files]},{3[calls]},{3[lines]}".format(f, c, l, stats)) + + def stress_test(self): + # For checking the overhead for each component: + def time_thing(thing): + per_thing = [] + pct_thing = [] + for _ in range(self.runs): + for n in range(self.numlo, self.numhi+1, self.step): + kwargs = { + "file_count": self.fixed, + "call_count": self.fixed, + "line_count": self.fixed, + } + kwargs[thing+"_count"] = n + res = self._compute_overhead(**kwargs) + per_thing.append(res.overhead / getattr(res, "{}s".format(thing))) + pct_thing.append(res.covered / res.baseline * 100) + + out = "Per {}: ".format(thing) + out += "mean = {:9.3f}us, stddev = {:8.3f}us, ".format( + statistics.mean(per_thing)*1e6, statistics.stdev(per_thing)*1e6 + ) + out += "min = {:9.3f}us, ".format(min(per_thing)*1e6) + out += "pct = {:6.1f}%, stddev = {:6.1f}%".format( + statistics.mean(pct_thing), statistics.stdev(pct_thing) + ) + print(out) + + time_thing("file") + time_thing("call") + time_thing("line") + + +if __name__ == '__main__': + with tempfile.TemporaryDirectory(prefix="coverage_stress_") as tempdir: + print("Working in {}".format(tempdir)) + os.chdir(tempdir) + sys.path.insert(0, ".") + + StressTest().stress_test() diff --git a/perf/solve_poly.py b/perf/solve_poly.py new file mode 100644 index 0000000..41365f4 --- /dev/null +++ b/perf/solve_poly.py @@ -0,0 +1,247 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +# Given empirical data from perf_measure.py, calculate the coefficients of the +# polynomials for file, call, and line operation counts. +# +# Written by Kyle Altendorf. + +import attr +import itertools +import numpy +import scipy.optimize +import sys + + +def f(*args, simplify=False): + p = ((),) + for l in range(len(args)): + l += 1 + p = itertools.chain(p, itertools.product(*(args,), repeat=l)) + + if simplify: + p = {tuple(sorted(set(x))) for x in p} + p = sorted(p, key=lambda x: (len(x), x)) + + return p + +def m(*args): + if len(args) == 0: + return 0 + + r = 1 + for arg in args: + r *= arg + + return r + + +class Poly: + def __init__(self, *names): + self.names = names + + self.terms = f(*self.names, simplify=True) + + def calculate(self, coefficients, **name_values): + for name in name_values: + if name not in self.names: + raise Exception('bad parameter') + + substituted_terms = [] + for term in self.terms: + substituted_terms.append(tuple(name_values[name] for name in term)) + + c_tuples = ((c,) for c in coefficients) + + terms = tuple(a + b for a, b in zip(c_tuples, substituted_terms)) + + multiplied = tuple(m(*t) for t in terms) + total = sum(multiplied) + + return total + + +poly = Poly('f', 'c', 'l') + +#print('\n'.join(str(t) for t in poly.terms)) + +@attr.s +class FCL: + f = attr.ib() + c = attr.ib() + l = attr.ib() + +INPUT = """\ +1,1,1,18,242,1119 +1,1,2,18,242,1121 +1,1,3,18,242,1123 +1,1,4,18,242,1125 +1,1,5,18,242,1127 +1,2,1,18,243,1124 +1,2,2,18,243,1128 +1,2,3,18,243,1132 +1,2,4,18,243,1136 +1,2,5,18,243,1140 +1,3,1,18,244,1129 +1,3,2,18,244,1135 +1,3,3,18,244,1141 +1,3,4,18,244,1147 +1,3,5,18,244,1153 +1,4,1,18,245,1134 +1,4,2,18,245,1142 +1,4,3,18,245,1150 +1,4,4,18,245,1158 +1,4,5,18,245,1166 +1,5,1,18,246,1139 +1,5,2,18,246,1149 +1,5,3,18,246,1159 +1,5,4,18,246,1169 +1,5,5,18,246,1179 +2,1,1,19,399,1893 +2,1,2,19,399,1897 +2,1,3,19,399,1901 +2,1,4,19,399,1905 +2,1,5,19,399,1909 +2,2,1,19,401,1903 +2,2,2,19,401,1911 +2,2,3,19,401,1919 +2,2,4,19,401,1927 +2,2,5,19,401,1935 +2,3,1,19,403,1913 +2,3,2,19,403,1925 +2,3,3,19,403,1937 +2,3,4,19,403,1949 +2,3,5,19,403,1961 +2,4,1,19,405,1923 +2,4,2,19,405,1939 +2,4,3,19,405,1955 +2,4,4,19,405,1971 +2,4,5,19,405,1987 +2,5,1,19,407,1933 +2,5,2,19,407,1953 +2,5,3,19,407,1973 +2,5,4,19,407,1993 +2,5,5,19,407,2013 +3,1,1,20,556,2667 +3,1,2,20,556,2673 +3,1,3,20,556,2679 +3,1,4,20,556,2685 +3,1,5,20,556,2691 +3,2,1,20,559,2682 +3,2,2,20,559,2694 +3,2,3,20,559,2706 +3,2,4,20,559,2718 +3,2,5,20,559,2730 +3,3,1,20,562,2697 +3,3,2,20,562,2715 +3,3,3,20,562,2733 +3,3,4,20,562,2751 +3,3,5,20,562,2769 +3,4,1,20,565,2712 +3,4,2,20,565,2736 +3,4,3,20,565,2760 +3,4,4,20,565,2784 +3,4,5,20,565,2808 +3,5,1,20,568,2727 +3,5,2,20,568,2757 +3,5,3,20,568,2787 +3,5,4,20,568,2817 +3,5,5,20,568,2847 +4,1,1,21,713,3441 +4,1,2,21,713,3449 +4,1,3,21,713,3457 +4,1,4,21,713,3465 +4,1,5,21,713,3473 +4,2,1,21,717,3461 +4,2,2,21,717,3477 +4,2,3,21,717,3493 +4,2,4,21,717,3509 +4,2,5,21,717,3525 +4,3,1,21,721,3481 +4,3,2,21,721,3505 +4,3,3,21,721,3529 +4,3,4,21,721,3553 +4,3,5,21,721,3577 +4,4,1,21,725,3501 +4,4,2,21,725,3533 +4,4,3,21,725,3565 +4,4,4,21,725,3597 +4,4,5,21,725,3629 +4,5,1,21,729,3521 +4,5,2,21,729,3561 +4,5,3,21,729,3601 +4,5,4,21,729,3641 +4,5,5,21,729,3681 +5,1,1,22,870,4215 +5,1,2,22,870,4225 +5,1,3,22,870,4235 +5,1,4,22,870,4245 +5,1,5,22,870,4255 +5,2,1,22,875,4240 +5,2,2,22,875,4260 +5,2,3,22,875,4280 +5,2,4,22,875,4300 +5,2,5,22,875,4320 +5,3,1,22,880,4265 +5,3,2,22,880,4295 +5,3,3,22,880,4325 +5,3,4,22,880,4355 +5,3,5,22,880,4385 +5,4,1,22,885,4290 +5,4,2,22,885,4330 +5,4,3,22,885,4370 +5,4,4,22,885,4410 +5,4,5,22,885,4450 +5,5,1,22,890,4315 +5,5,2,22,890,4365 +5,5,3,22,890,4415 +5,5,4,22,890,4465 +5,5,5,22,890,4515 +""" + +inputs_outputs = {} +for row in INPUT.splitlines(): + row = [int(v) for v in row.split(",")] + inputs_outputs[FCL(*row[:3])] = FCL(*row[3:]) + +#print('\n'.join(str(t) for t in inputs_outputs.items())) + +def calc_poly_coeff(poly, coefficients): + c_tuples = list(((c,) for c in coefficients)) + poly = list(f(*poly)) + poly = list(a + b for a, b in zip(c_tuples, poly)) + multiplied = list(m(*t) for t in poly) + total = sum(multiplied) + return total + +def calc_error(inputs, output, coefficients): + result = poly.calculate(coefficients, **inputs) + return result - output + + +def calc_total_error(inputs_outputs, coefficients, name): + total_error = 0 + for inputs, outputs in inputs_outputs.items(): + total_error += abs(calc_error(attr.asdict(inputs), attr.asdict(outputs)[name], coefficients)) + + return total_error + +coefficient_count = len(poly.terms) +#print('count: {}'.format(coefficient_count)) +x0 = numpy.array((0,) * coefficient_count) + +#print(x0) + +with open('results', 'w') as f: + for name in sorted(attr.asdict(FCL(0,0,0))): + c = scipy.optimize.minimize( + fun=lambda c: calc_total_error(inputs_outputs, c, name), + x0=x0 + ) + + coefficients = [int(round(x)) for x in c.x] + terms = [''.join(t) for t in poly.terms] + message = "{}' = ".format(name) + message += ' + '.join("{}{}".format(coeff if coeff != 1 else '', term) for coeff, term in reversed(list(zip(coefficients, terms))) if coeff != 0) + print(message) + f.write(message) @@ -2,13 +2,13 @@ # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt # lint Python modules using external checkers. -# +# # This is the main checker controlling the other ones and the reports # generation. It is itself both a raw checker and an astng checker in order # to: # * handle message activation / deactivation at the module level # * handle some basic but necessary stats'data (number of classes, methods...) -# +# [MASTER] # Specify a configuration file. @@ -54,7 +54,7 @@ enable= useless-suppression # Disable the message(s) with the given id(s). -disable= +disable= spelling, # Messages that are just silly: locally-disabled, @@ -63,11 +63,13 @@ disable= bad-whitespace, global-statement, broad-except, + no-else-return, # Messages that may be silly: no-self-use, no-member, using-constant-test, too-many-nested-blocks, + too-many-ancestors, # Formatting stuff superfluous-parens,bad-continuation, # I'm fine deciding my own import order, @@ -95,6 +97,9 @@ files-output=no # Tells wether to display a full report or only the messages reports=no +# I don't need a score, thanks. +score=no + # Python expression which should return a note less than 10 (10 is the highest # note).You have access to the variables errors warning, statement which # respectively contain the number of errors / warnings messages and the total @@ -118,7 +123,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme # * dangerous default values as arguments # * redefinition of function / method / class # * uses of the global statement -# +# [BASIC] # Regular expression which should only match functions or classes name which do @@ -126,8 +131,9 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme # Special methods don't: __foo__ # Test methods don't: testXXXX # TestCase overrides don't: setUp, tearDown +# Nested decorator implementations: _decorator, _wrapped # Dispatched methods don't: _xxx__Xxxx -no-docstring-rgx=__.*__|test[A-Z_].*|setUp|tearDown|_.*__.* +no-docstring-rgx=__.*__|test[A-Z_].*|setUp|tearDown|_decorator|_wrapped|_.*__.* # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ @@ -168,7 +174,7 @@ bad-functions= # try to find bugs in the code using type inference -# +# [TYPECHECK] # Tells wether missing members accessed in mixin class should be ignored. A @@ -189,14 +195,15 @@ acquired-members=REQUEST,acl_users,aq_parent # * undefined variables # * redefinition of variable from builtins or from an outer scope # * use of variable before assigment -# +# [VARIABLES] # Tells wether we should check for unused import in __init__ files. init-import=no -# A regular expression matching names used for dummy variables (i.e. not used). -dummy-variables-rgx=_|dummy|unused|.*_unused +# A regular expression matching names of unused arguments. +ignored-argument-names=_|unused|.*_unused +dummy-variables-rgx=_|unused|.*_unused # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. @@ -210,7 +217,7 @@ additional-builtins= # * attributes not defined in the __init__ method # * supported interfaces implementation # * unreachable code -# +# [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. @@ -220,7 +227,7 @@ defining-attr-methods=__init__,__new__,setUp,reset # checks for sign of poor/misdesign: # * number of methods, attributes, local variables... # * size, complexity of functions, methods -# +# [DESIGN] # Maximum number of arguments for function / method @@ -256,7 +263,7 @@ max-public-methods=500 # * relative / wildcard imports # * cyclic imports # * uses of deprecated modules -# +# [IMPORTS] # Deprecated modules which should not be used, separated by a comma @@ -280,7 +287,7 @@ int-import-graph= # * strict indentation # * line length # * use of <> instead of != -# +# [FORMAT] # Maximum number of characters on a single line. @@ -297,7 +304,7 @@ indent-string=' ' # checks for: # * warning notes in the code like FIXME, XXX # * PEP 263: source code with non ascii character but no encoding declaration -# +# [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. @@ -307,7 +314,7 @@ notes=FIXME,XXX,TODO # checks for similarities and duplicated code. This computation may be # memory / CPU intensive, so you should disable it if you experiments some # problems. -# +# [SIMILARITIES] # Minimum lines number of a similarity. diff --git a/requirements/ci.pip b/requirements/ci.pip new file mode 100644 index 0000000..0c560d4 --- /dev/null +++ b/requirements/ci.pip @@ -0,0 +1,7 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +# Things CI servers need to succeeed. +-r tox.pip +-r pytest.pip +-r wheel.pip diff --git a/requirements/dev.pip b/requirements/dev.pip index 29be753..b211363 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -5,17 +5,17 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ # PyPI requirements for running tests. -nose==1.3.7 -r tox.pip +-r pytest.pip # for linting. -greenlet==0.4.10 +greenlet==0.4.12 mock==2.0.0 -PyContracts==1.7.9 -pyenchant==1.6.7 -pylint==1.6.4 -unittest-mixins==1.1.1 +PyContracts==1.7.15 +pyenchant==1.6.8 +pylint==1.7.1 +unittest-mixins==1.3 # for kitting. -requests==2.10.0 -twine==1.7.4 +requests==2.13.0 +twine==1.8.1 diff --git a/requirements/pytest.pip b/requirements/pytest.pip new file mode 100644 index 0000000..07ca979 --- /dev/null +++ b/requirements/pytest.pip @@ -0,0 +1,8 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +# The pytest specifics used by coverage.py +pytest==3.0.7 +pytest-xdist==1.16.0 +pytest-warnings==0.2.0 +flaky==3.3.0 diff --git a/requirements/tox.pip b/requirements/tox.pip index 86093d5..d09412d 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -1,2 +1,4 @@ # The version of tox used by coverage.py -tox==2.3.1 +tox==2.7.0 +# Adds env recreation on requirements file changes. +tox-battery==0.4 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 426042c..dd2e6ec 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,3 +1,3 @@ # Things needed to make wheels for coverage.py -setuptools==25.1.6 +setuptools==35.0.2 wheel==0.29.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1f27d90 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,13 @@ +[tool:pytest] +addopts = -q -n3 --strict --no-flaky-report +markers = + expensive: too slow to run during "make smoke" + +[pep8] +# E265 block comment should start with '# ' +# E266 too many leading '#' for block comment +# E301 expected 1 blank line, found 0 +# E401 multiple imports on one line +# The rest are the default ignored warnings. +ignore = E265,E266,E123,E133,E226,E241,E242,E301,E401 +max-line-length = 100 @@ -30,6 +30,8 @@ Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy +Programming Language :: Python :: Implementation :: Jython +Programming Language :: Python :: Implementation :: IronPython Topic :: Software Development :: Quality Assurance Topic :: Software Development :: Testing """ @@ -46,6 +48,11 @@ with open(cov_ver_py) as version_file: with open("README.rst") as readme: long_description = readme.read().replace("http://coverage.readthedocs.io", __url__) +with open("CONTRIBUTORS.txt", "rb") as contributors: + paras = contributors.read().split(b"\n\n") + num_others = len(paras[-1].splitlines()) + num_others += 1 # Count Gareth Rees, who is mentioned in the top paragraph. + classifier_list = classifiers.splitlines() if version_info[3] == 'alpha': @@ -70,6 +77,7 @@ setup_args = dict( package_data={ 'coverage': [ 'htmlfiles/*.*', + 'fullcoverage/*.*', ] }, @@ -86,7 +94,7 @@ setup_args = dict( # We need to get HTML assets from our htmlfiles directory. zip_safe=False, - author='Ned Batchelder and others', + author='Ned Batchelder and {0} others'.format(num_others), author_email='ned@nedbatchelder.com', description=doc, long_description=long_description, diff --git a/tests/__init__.py b/tests/__init__.py index 5a0e30f..1ff1e1b 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1,4 @@ -"""Automated tests. Run with nosetests.""" +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Automated tests. Run with pytest.""" diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 9410e07..0e6131f 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -5,13 +5,12 @@ import contextlib import datetime -import glob import os import random import re import shlex -import shutil import sys +import types from unittest_mixins import ( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, @@ -19,24 +18,51 @@ from unittest_mixins import ( ) import coverage -from coverage.backunittest import TestCase +from coverage import env +from coverage.backunittest import TestCase, unittest from coverage.backward import StringIO, import_local_file, string_class, shlex_quote from coverage.cmdline import CoverageScript -from coverage.debug import _TEST_NAME_FILE, DebugControl +from coverage.debug import _TEST_NAME_FILE +from coverage.misc import StopEverything -from tests.helpers import run_command +from tests.helpers import run_command, SuperModuleCleaner # Status returns for the command line. OK, ERR = 0, 1 +def convert_skip_exceptions(method): + """A decorator for test methods to convert StopEverything to SkipTest.""" + def wrapper(*args, **kwargs): + """Run the test method, and convert exceptions.""" + try: + result = method(*args, **kwargs) + except StopEverything: + raise unittest.SkipTest("StopEverything!") + return result + return wrapper + + +class SkipConvertingMetaclass(type): + """Decorate all test methods to convert StopEverything to SkipTest.""" + def __new__(mcs, name, bases, attrs): + for attr_name, attr_value in attrs.items(): + if attr_name.startswith('test_') and isinstance(attr_value, types.FunctionType): + attrs[attr_name] = convert_skip_exceptions(attr_value) + + return super(SkipConvertingMetaclass, mcs).__new__(mcs, name, bases, attrs) + + +CoverageTestMethodsMixin = SkipConvertingMetaclass('CoverageTestMethodsMixin', (), {}) + class CoverageTest( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, DelayedAssertionMixin, - TestCase + CoverageTestMethodsMixin, + TestCase, ): """A base class for coverage.py test cases.""" @@ -46,9 +72,14 @@ class CoverageTest( # Tell newer unittest implementations to print long helpful messages. longMessage = True + # Let stderr go to stderr, pytest will capture it for us. + show_stderr = True + def setUp(self): super(CoverageTest, self).setUp() + self.module_cleaner = SuperModuleCleaner() + # Attributes for getting info about what happened. self.last_command_status = None self.last_command_output = None @@ -67,25 +98,7 @@ class CoverageTest( one test. """ - # So that we can re-import files, clean them out first. - self.cleanup_modules() - # Also have to clean out the .pyc file, since the timestamp - # resolution is only one second, a changed file might not be - # picked up. - for pyc in glob.glob('*.pyc'): - os.remove(pyc) - if os.path.exists("__pycache__"): - shutil.rmtree("__pycache__") - - def import_local_file(self, modname, modfile=None): - """Import a local file as a module. - - Opens a file in the current directory named `modname`.py, imports it - as `modname`, and returns the module object. `modfile` is the file to - import if it isn't in the current directory. - - """ - return import_local_file(modname, modfile) + self.module_cleaner.clean_local_file_imports() def start_import_stop(self, cov, modname, modfile=None): """Start coverage, import a file, then stop coverage. @@ -100,7 +113,7 @@ class CoverageTest( cov.start() try: # pragma: nested # Import the Python file, executing it. - mod = self.import_local_file(modname, modfile) + mod = import_local_file(modname, modfile) finally: # pragma: nested # Stop coverage.py. cov.stop() @@ -237,17 +250,17 @@ class CoverageTest( with self.delayed_assertions(): self.assert_equal_args( analysis.arc_possibilities(), arcs, - "Possible arcs differ", + "Possible arcs differ: minus is actual, plus is expected" ) self.assert_equal_args( analysis.arcs_missing(), arcs_missing, - "Missing arcs differ" + "Missing arcs differ: minus is actual, plus is expected" ) self.assert_equal_args( analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ" + "Unpredicted arcs differ: minus is actual, plus is expected" ) if report: @@ -259,11 +272,27 @@ class CoverageTest( return cov @contextlib.contextmanager - def assert_warnings(self, cov, warnings): - """A context manager to check that particular warnings happened in `cov`.""" + def assert_warnings(self, cov, warnings, not_warnings=()): + """A context manager to check that particular warnings happened in `cov`. + + `cov` is a Coverage instance. `warnings` is a list of regexes. Every + regex must match a warning that was issued by `cov`. It is OK for + extra warnings to be issued by `cov` that are not matched by any regex. + Warnings that are disabled are still considered issued by this function. + + `not_warnings` is a list of regexes that must not appear in the + warnings. This is only checked if there are some positive warnings to + test for in `warnings`. + + If `warnings` is empty, then `cov` is not allowed to issue any + warnings. + + """ saved_warnings = [] - def capture_warning(msg): + def capture_warning(msg, slug=None): """A fake implementation of Coverage._warn, to capture warnings.""" + if slug: + msg = "%s (%s)" % (msg, slug) saved_warnings.append(msg) original_warn = cov._warn @@ -274,12 +303,22 @@ class CoverageTest( except: raise else: - for warning_regex in warnings: - for saved in saved_warnings: - if re.search(warning_regex, saved): - break - else: - self.fail("Didn't find warning %r in %r" % (warning_regex, saved_warnings)) + if warnings: + for warning_regex in warnings: + for saved in saved_warnings: + if re.search(warning_regex, saved): + break + else: + self.fail("Didn't find warning %r in %r" % (warning_regex, saved_warnings)) + for warning_regex in not_warnings: + for saved in saved_warnings: + if re.search(warning_regex, saved): + self.fail("Found warning %r in %r" % (warning_regex, saved_warnings)) + else: + # No warnings expected. Raise if any warnings happened. + if saved_warnings: + self.fail("Unexpected warnings: %r" % (saved_warnings,)) + finally: cov._warn = original_warn def nice_file(self, *fparts): @@ -328,8 +367,7 @@ class CoverageTest( Returns None. """ - script = CoverageScript(_covpkg=_covpkg) - ret_actual = script.command_line(shlex.split(args)) + ret_actual = command_line(args, _covpkg=_covpkg) self.assertEqual(ret_actual, ret) coverage_command = "coverage" @@ -373,7 +411,7 @@ class CoverageTest( """ # Make sure "python" and "coverage" mean specifically what we want # them to mean. - split_commandline = cmd.split(" ", 1) + split_commandline = cmd.split() command_name = split_commandline[0] command_args = split_commandline[1:] @@ -383,30 +421,49 @@ class CoverageTest( # get executed as "python3.3 foo.py". This is important because # Python 3.x doesn't install as "python", so you might get a Python # 2 executable instead if you don't use the executable's basename. - command_name = os.path.basename(sys.executable) + command_words = [os.path.basename(sys.executable)] + + elif command_name == "coverage": + if env.JYTHON: # pragma: only jython + # Jython can't do reporting, so let's skip the test now. + if command_args and command_args[0] in ('report', 'html', 'xml', 'annotate'): + self.skipTest("Can't run reporting commands in Jython") + # Jython can't run "coverage" as a command because the shebang + # refers to another shebang'd Python script. So run them as + # modules. + command_words = "jython -m coverage".split() + else: + # The invocation requests the Coverage.py program. Substitute the + # actual Coverage.py main command name. + command_words = [self.coverage_command] - if command_name == "coverage": - # The invocation requests the Coverage.py program. Substitute the - # actual Coverage.py main command name. - command_name = self.coverage_command + else: + command_words = [command_name] - cmd = " ".join([shlex_quote(command_name)] + command_args) + cmd = " ".join([shlex_quote(w) for w in command_words] + command_args) # Add our test modules directory to PYTHONPATH. I'm sure there's too # much path munging here, but... - here = os.path.dirname(self.nice_file(coverage.__file__, "..")) - testmods = self.nice_file(here, 'tests/modules') - zipfile = self.nice_file(here, 'tests/zipmods.zip') - pypath = os.getenv('PYTHONPATH', '') + pythonpath_name = "PYTHONPATH" + if env.JYTHON: + pythonpath_name = "JYTHONPATH" # pragma: only jython + + testmods = self.nice_file(self.working_root(), 'tests/modules') + zipfile = self.nice_file(self.working_root(), 'tests/zipmods.zip') + pypath = os.getenv(pythonpath_name, '') if pypath: pypath += os.pathsep pypath += testmods + os.pathsep + zipfile - self.set_environ('PYTHONPATH', pypath) + self.set_environ(pythonpath_name, pypath) self.last_command_status, self.last_command_output = run_command(cmd) print(self.last_command_output) return self.last_command_status, self.last_command_output + def working_root(self): + """Where is the root of the coverage.py working tree?""" + return os.path.dirname(self.nice_file(coverage.__file__, "..")) + def report_from_command(self, cmd): """Return the report from the `cmd`, with some convenience added.""" report = self.run_command(cmd).replace('\\', '/') @@ -433,11 +490,14 @@ class CoverageTest( return self.squeezed_lines(report)[-1] -class DebugControlString(DebugControl): - """A `DebugControl` that writes to a StringIO, for testing.""" - def __init__(self, options): - super(DebugControlString, self).__init__(options, StringIO()) +def command_line(args, **kwargs): + """Run `args` through the CoverageScript command line. + + `kwargs` are the keyword arguments to the CoverageScript constructor. + + Returns the return code from CoverageScript.command_line. - def get_output(self): - """Get the output text from the `DebugControl`.""" - return self.output.getvalue() + """ + script = CoverageScript(**kwargs) + ret = script.command_line(shlex.split(args)) + return ret diff --git a/tests/farm/annotate/run_encodings.py b/tests/farm/annotate/run_encodings.py index 527cd88..46d8c64 100644 --- a/tests/farm/annotate/run_encodings.py +++ b/tests/farm/annotate/run_encodings.py @@ -1,10 +1,10 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -copy("src", "out") +copy("src", "out_encodings") run(""" coverage run utf8.py coverage annotate utf8.py - """, rundir="out") -compare("out", "gold_encodings", "*,cover") -clean("out") + """, rundir="out_encodings") +compare("out_encodings", "gold_encodings", "*,cover") +clean("out_encodings") diff --git a/tests/farm/html/gold_x_xml/coverage.xml b/tests/farm/html/gold_x_xml/coverage.xml index b3e9854..162824a 100644 --- a/tests/farm/html/gold_x_xml/coverage.xml +++ b/tests/farm/html/gold_x_xml/coverage.xml @@ -1,7 +1,7 @@ <?xml version="1.0" ?> -<coverage branch-rate="0" line-rate="0.6667" timestamp="1437745880639" version="4.0a7"> +<coverage branch-rate="0" branches-covered="0" branches-valid="0" complexity="0" line-rate="0.6667" lines-covered="2" lines-valid="3" timestamp="1437745880639" version="4.0a7"> <!-- Generated by coverage.py: https://coverage.readthedocs.io/en/coverage-4.0a7 --> - <!-- Based on https://raw.githubusercontent.com/cobertura/web/f0366e5e2cf18f111cbd61fc34ef720a6584ba02/htdocs/xml/coverage-03.dtd --> + <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd --> <sources> <source>/Users/ned/coverage/trunk/tests/farm/html/src</source> </sources> diff --git a/tests/farm/html/gold_y_xml_branch/coverage.xml b/tests/farm/html/gold_y_xml_branch/coverage.xml index d8ff0bb..bcf1137 100644 --- a/tests/farm/html/gold_y_xml_branch/coverage.xml +++ b/tests/farm/html/gold_y_xml_branch/coverage.xml @@ -1,7 +1,7 @@ <?xml version="1.0" ?> -<coverage branch-rate="0.5" line-rate="0.8" timestamp="1437745880882" version="4.0a7"> +<coverage branch-rate="0.5" branches-covered="1" branches-valid="2" complexity="0" line-rate="0.8" lines-covered="4" lines-valid="5" timestamp="1437745880882" version="4.0a7"> <!-- Generated by coverage.py: https://coverage.readthedocs.io/en/coverage-4.0a7 --> - <!-- Based on https://raw.githubusercontent.com/cobertura/web/f0366e5e2cf18f111cbd61fc34ef720a6584ba02/htdocs/xml/coverage-03.dtd --> + <!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd --> <sources> <source>/Users/ned/coverage/trunk/tests/farm/html/src</source> </sources> diff --git a/tests/farm/html/src/partial.ini b/tests/farm/html/src/partial.ini new file mode 100644 index 0000000..cdb241b --- /dev/null +++ b/tests/farm/html/src/partial.ini @@ -0,0 +1,9 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +[run] +branch = True + +[report] +exclude_lines = + raise AssertionError diff --git a/tests/farm/html/src/partial.py b/tests/farm/html/src/partial.py index 66dddac..0f8fbe3c 100644 --- a/tests/farm/html/src/partial.py +++ b/tests/farm/html/src/partial.py @@ -1,9 +1,9 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -# partial branches +# partial branches and excluded lines -a = 3 +a = 6 while True: break @@ -18,4 +18,7 @@ if 0: never_happen() if 1: - a = 13 + a = 21 + +if a == 23: + raise AssertionError("Can't") diff --git a/tests/farm/run/run_chdir.py b/tests/farm/run/run_chdir.py index 9e3c751..1da4e9a 100644 --- a/tests/farm/run/run_chdir.py +++ b/tests/farm/run/run_chdir.py @@ -1,15 +1,15 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -copy("src", "out") +copy("src", "out_chdir") run(""" coverage run chdir.py coverage report - """, rundir="out", outfile="stdout.txt") -contains("out/stdout.txt", + """, rundir="out_chdir", outfile="stdout.txt") +contains("out_chdir/stdout.txt", "Line One", "Line Two", "chdir" ) -doesnt_contain("out/stdout.txt", "No such file or directory") -clean("out") +doesnt_contain("out_chdir/stdout.txt", "No such file or directory") +clean("out_chdir") diff --git a/tests/farm/run/run_timid.py b/tests/farm/run/run_timid.py index a632cea..0370cf8 100644 --- a/tests/farm/run/run_timid.py +++ b/tests/farm/run/run_timid.py @@ -17,16 +17,16 @@ import os if os.environ.get('COVERAGE_COVERAGE', ''): skip("Can't test timid during coverage measurement.") -copy("src", "out") +copy("src", "out_timid") run(""" python showtrace.py none coverage run showtrace.py regular coverage run --timid showtrace.py timid - """, rundir="out", outfile="showtraceout.txt") + """, rundir="out_timid", outfile="showtraceout.txt") # When running without coverage, no trace function # When running timidly, the trace function is always Python. -contains("out/showtraceout.txt", +contains("out_timid/showtraceout.txt", "none None", "timid PyTracer", ) @@ -34,10 +34,10 @@ contains("out/showtraceout.txt", if os.environ.get('COVERAGE_TEST_TRACER', 'c') == 'c': # If the C trace function is being tested, then regular running should have # the C function, which registers itself as f_trace. - contains("out/showtraceout.txt", "regular CTracer") + contains("out_timid/showtraceout.txt", "regular CTracer") else: # If the Python trace function is being tested, then regular running will # also show the Python function. - contains("out/showtraceout.txt", "regular PyTracer") + contains("out_timid/showtraceout.txt", "regular PyTracer") -clean("out") +clean("out_timid") diff --git a/tests/farm/run/run_xxx.py b/tests/farm/run/run_xxx.py index 62a862e..1db5b0d 100644 --- a/tests/farm/run/run_xxx.py +++ b/tests/farm/run/run_xxx.py @@ -1,15 +1,15 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -copy("src", "out") +copy("src", "out_xxx") run(""" coverage run xxx coverage report - """, rundir="out", outfile="stdout.txt") -contains("out/stdout.txt", + """, rundir="out_xxx", outfile="stdout.txt") +contains("out_xxx/stdout.txt", "xxx: 3 4 0 7", "\nxxx ", # The reporting line for xxx " 7 1 86%" # The reporting data for xxx ) -doesnt_contain("out/stdout.txt", "No such file or directory") -clean("out") +doesnt_contain("out_xxx/stdout.txt", "No such file or directory") +clean("out_xxx") diff --git a/tests/goldtest.py b/tests/goldtest.py index 27a082e..baaa8f0 100644 --- a/tests/goldtest.py +++ b/tests/goldtest.py @@ -38,5 +38,5 @@ class CoverageGoldTest(CoverageTest): # beginning of the test. clean(the_dir) - if not os.environ.get("COVERAGE_KEEP_OUTPUT"): # pragma: partial + if not os.environ.get("COVERAGE_KEEP_OUTPUT"): # pragma: part covered self.addCleanup(clean, the_dir) diff --git a/tests/helpers.py b/tests/helpers.py index f4bff2b..f10169a 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -3,11 +3,18 @@ """Helpers for coverage.py tests.""" +import glob +import itertools import os +import re +import shutil import subprocess import sys +from unittest_mixins import ModuleCleaner + from coverage import env +from coverage.backward import invalidate_import_caches, unicode_class from coverage.misc import output_encoding @@ -17,16 +24,14 @@ def run_command(cmd): Returns the exit status code and the combined stdout and stderr. """ - if env.PY2 and isinstance(cmd, unicode): + if env.PY2 and isinstance(cmd, unicode_class): cmd = cmd.encode(sys.getfilesystemencoding()) # In some strange cases (PyPy3 in a virtualenv!?) the stdout encoding of # the subprocess is set incorrectly to ascii. Use an environment variable # to force the encoding to be the same as ours. sub_env = dict(os.environ) - encoding = output_encoding() - if encoding: - sub_env['PYTHONIOENCODING'] = encoding + sub_env['PYTHONIOENCODING'] = output_encoding() proc = subprocess.Popen( cmd, @@ -53,11 +58,19 @@ class CheckUniqueFilenames(object): self.wrapped = wrapped @classmethod - def hook(cls, cov, method_name): - """Replace a method with our checking wrapper.""" - method = getattr(cov, method_name) + def hook(cls, obj, method_name): + """Replace a method with our checking wrapper. + + The method must take a string as a first argument. That argument + will be checked for uniqueness across all the calls to this method. + + The values don't have to be file names actually, just strings, but + we only use it for filename arguments. + + """ + method = getattr(obj, method_name) hook = cls(method) - setattr(cov, method_name, hook.wrapper) + setattr(obj, method_name, hook.wrapper) return hook def wrapper(self, filename, *args, **kwargs): @@ -68,3 +81,50 @@ class CheckUniqueFilenames(object): self.filenames.add(filename) ret = self.wrapped(filename, *args, **kwargs) return ret + + +def re_lines(text, pat, match=True): + """Return the text of lines that match `pat` in the string `text`. + + If `match` is false, the selection is inverted: only the non-matching + lines are included. + + Returns a string, the text of only the selected lines. + + """ + return "".join(l for l in text.splitlines(True) if bool(re.search(pat, l)) == match) + + +def re_line(text, pat): + """Return the one line in `text` that matches regex `pat`. + + Raises an AssertionError if more than one, or less than one, line matches. + + """ + lines = re_lines(text, pat).splitlines() + assert len(lines) == 1 + return lines[0] + + +class SuperModuleCleaner(ModuleCleaner): + """Remember the state of sys.modules and restore it later.""" + + def clean_local_file_imports(self): + """Clean up the results of calls to `import_local_file`. + + Use this if you need to `import_local_file` the same file twice in + one test. + + """ + # So that we can re-import files, clean them out first. + self.cleanup_modules() + + # Also have to clean out the .pyc file, since the timestamp + # resolution is only one second, a changed file might not be + # picked up. + for pyc in itertools.chain(glob.glob('*.pyc'), glob.glob('*$py.class')): + os.remove(pyc) + if os.path.exists("__pycache__"): + shutil.rmtree("__pycache__") + + invalidate_import_caches() diff --git a/tests/modules/process_test/try_execfile.py b/tests/modules/process_test/try_execfile.py index 7090507..ec7dcbe 100644 --- a/tests/modules/process_test/try_execfile.py +++ b/tests/modules/process_test/try_execfile.py @@ -20,7 +20,10 @@ differences and get a clean diff. """ -import json, os, sys +import itertools +import json +import os +import sys # sys.path varies by execution environments. Coverage.py uses setuptools to # make console scripts, which means pkg_resources is imported. pkg_resources @@ -65,19 +68,28 @@ FN_VAL = my_function("fooey") loader = globals().get('__loader__') fullname = getattr(loader, 'fullname', None) or getattr(loader, 'name', None) +# A more compact grouped-by-first-letter list of builtins. +def word_group(w): + """Clump AB, CD, EF, etc.""" + return chr((ord(w[0]) + 1) & 0xFE) + +builtin_dir = [" ".join(s) for _, s in itertools.groupby(dir(__builtins__), key=word_group)] + globals_to_check = { + 'os.getcwd': os.getcwd(), '__name__': __name__, '__file__': __file__, '__doc__': __doc__, '__builtins__.has_open': hasattr(__builtins__, 'open'), - '__builtins__.dir': dir(__builtins__), + '__builtins__.dir': builtin_dir, '__loader__ exists': loader is not None, '__loader__.fullname': fullname, '__package__': __package__, 'DATA': DATA, 'FN_VAL': FN_VAL, '__main__.DATA': getattr(__main__, "DATA", "nothing"), - 'argv': sys.argv, + 'argv0': sys.argv[0], + 'argv1-n': sys.argv[1:], 'path': cleaned_sys_path, } diff --git a/tests/modules/usepkgs.py b/tests/modules/usepkgs.py index 4e94aca..222e68c 100644 --- a/tests/modules/usepkgs.py +++ b/tests/modules/usepkgs.py @@ -1,7 +1,7 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -import pkg1.p1a, pkg1.p1b +import pkg1.p1a, pkg1.p1b, pkg1.sub import pkg2.p2a, pkg2.p2b import othermods.othera, othermods.otherb import othermods.sub.osa, othermods.sub.osb diff --git a/tests/osinfo.py b/tests/osinfo.py index a7ebd2e..094fb09 100644 --- a/tests/osinfo.py +++ b/tests/osinfo.py @@ -34,8 +34,8 @@ if env.WINDOWS: ctypes.byref(mem_struct), ctypes.sizeof(mem_struct) ) - if not ret: - return 0 + if not ret: # pragma: part covered + return 0 # pragma: cant happen return mem_struct.PrivateUsage elif env.LINUX: @@ -50,13 +50,13 @@ elif env.LINUX: # Get pseudo file /proc/<pid>/status with open('/proc/%d/status' % os.getpid()) as t: v = t.read() - except IOError: + except IOError: # pragma: cant happen return 0 # non-Linux? # Get VmKey line e.g. 'VmRSS: 9999 kB\n ...' i = v.index(key) v = v[i:].split(None, 3) - if len(v) < 3: - return 0 # Invalid format? + if len(v) < 3: # pragma: part covered + return 0 # pragma: cant happen # Convert Vm value to bytes. return int(float(v[1]) * _scale[v[2].lower()]) diff --git a/tests/plugin1.py b/tests/plugin1.py index af4dfc5..63ebacf 100644 --- a/tests/plugin1.py +++ b/tests/plugin1.py @@ -24,7 +24,7 @@ class FileTracer(coverage.FileTracer): """A FileTracer emulating a simple static plugin.""" def __init__(self, filename): - """Claim that xyz.py was actually sourced from ABC.zz""" + """Claim that */*xyz.py was actually sourced from /src/*ABC.zz""" self._filename = filename self._source_filename = os.path.join( "/src", diff --git a/tests/test_api.py b/tests/test_api.py index 6f14210..9a3fc82 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -11,11 +11,11 @@ import warnings import coverage from coverage import env -from coverage.backward import StringIO +from coverage.backward import StringIO, import_local_file from coverage.misc import CoverageException from coverage.report import Reporter -from tests.coveragetest import CoverageTest +from tests.coveragetest import CoverageTest, CoverageTestMethodsMixin class ApiTest(CoverageTest): @@ -35,7 +35,7 @@ class ApiTest(CoverageTest): def assertFiles(self, files): """Assert that the files here are `files`, ignoring the usual junk.""" here = os.listdir(".") - here = self.clean_files(here, ["*.pyc", "__pycache__"]) + here = self.clean_files(here, ["*.pyc", "__pycache__", "*$py.class"]) self.assertCountEqual(here, files) def test_unexecuted_file(self): @@ -282,17 +282,70 @@ class ApiTest(CoverageTest): self.check_code1_code2(cov) def test_start_save_stop(self): - self.skipTest("Expected failure: https://bitbucket.org/ned/coveragepy/issue/79") self.make_code1_code2() cov = coverage.Coverage() cov.start() - self.import_local_file("code1") - cov.save() - self.import_local_file("code2") - cov.stop() - + import_local_file("code1") # pragma: nested + cov.save() # pragma: nested + import_local_file("code2") # pragma: nested + cov.stop() # pragma: nested self.check_code1_code2(cov) + def test_start_save_nostop(self): + self.make_code1_code2() + cov = coverage.Coverage() + cov.start() + import_local_file("code1") # pragma: nested + cov.save() # pragma: nested + import_local_file("code2") # pragma: nested + self.check_code1_code2(cov) # pragma: nested + # Then stop it, or the test suite gets out of whack. + cov.stop() # pragma: nested + + def test_two_getdata_only_warn_once(self): + self.make_code1_code2() + cov = coverage.Coverage(source=["."], omit=["code1.py"]) + cov.start() + import_local_file("code1") # pragma: nested + cov.stop() # pragma: nested + # We didn't collect any data, so we should get a warning. + with self.assert_warnings(cov, ["No data was collected"]): + cov.get_data() + # But calling get_data a second time with no intervening activity + # won't make another warning. + with self.assert_warnings(cov, []): + cov.get_data() + + def test_two_getdata_only_warn_once_nostop(self): + self.make_code1_code2() + cov = coverage.Coverage(source=["."], omit=["code1.py"]) + cov.start() + import_local_file("code1") # pragma: nested + # We didn't collect any data, so we should get a warning. + with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested + cov.get_data() # pragma: nested + # But calling get_data a second time with no intervening activity + # won't make another warning. + with self.assert_warnings(cov, []): # pragma: nested + cov.get_data() # pragma: nested + # Then stop it, or the test suite gets out of whack. + cov.stop() # pragma: nested + + def test_two_getdata_warn_twice(self): + self.make_code1_code2() + cov = coverage.Coverage(source=["."], omit=["code1.py", "code2.py"]) + cov.start() + import_local_file("code1") # pragma: nested + # We didn't collect any data, so we should get a warning. + with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested + cov.save() # pragma: nested + import_local_file("code2") # pragma: nested + # Calling get_data a second time after tracing some more will warn again. + with self.assert_warnings(cov, ["No data was collected"]): # pragma: nested + cov.get_data() # pragma: nested + # Then stop it, or the test suite gets out of whack. + cov.stop() # pragma: nested + def make_good_data_files(self): """Make some good data files.""" self.make_code1_code2() @@ -348,6 +401,49 @@ class ApiTest(CoverageTest): self.assertEqual(statements, [1, 2]) self.assertEqual(missing, [1, 2]) + def test_warnings(self): + self.make_file("hello.py", """\ + import sys, os + print("Hello") + """) + cov = coverage.Coverage(source=["sys", "xyzzy", "quux"]) + self.start_import_stop(cov, "hello") + cov.get_data() + + out = self.stdout() + self.assertIn("Hello\n", out) + + err = self.stderr() + self.assertIn(textwrap.dedent("""\ + Coverage.py warning: Module sys has no Python source. (module-not-python) + Coverage.py warning: Module xyzzy was never imported. (module-not-imported) + Coverage.py warning: Module quux was never imported. (module-not-imported) + Coverage.py warning: No data was collected. (no-data-collected) + """), err) + + def test_warnings_suppressed(self): + self.make_file("hello.py", """\ + import sys, os + print("Hello") + """) + self.make_file(".coveragerc", """\ + [run] + disable_warnings = no-data-collected, module-not-imported + """) + cov = coverage.Coverage(source=["sys", "xyzzy", "quux"]) + self.start_import_stop(cov, "hello") + cov.get_data() + + out = self.stdout() + self.assertIn("Hello\n", out) + + err = self.stderr() + self.assertIn(textwrap.dedent("""\ + Coverage.py warning: Module sys has no Python source. (module-not-python) + """), err) + self.assertNotIn("module-not-imported", err) + self.assertNotIn("no-data-collected", err) + class NamespaceModuleTest(CoverageTest): """Test PEP-420 namespace modules.""" @@ -376,16 +472,14 @@ class UsingModulesMixin(object): def setUp(self): super(UsingModulesMixin, self).setUp() - old_dir = os.getcwd() - os.chdir(self.nice_file(os.path.dirname(__file__), 'modules')) - self.addCleanup(os.chdir, old_dir) + self.chdir(self.nice_file(os.path.dirname(__file__), 'modules')) # Parent class saves and restores sys.path, we can just modify it. sys.path.append(".") sys.path.append("../moremodules") -class OmitIncludeTestsMixin(UsingModulesMixin): +class OmitIncludeTestsMixin(UsingModulesMixin, CoverageTestMethodsMixin): """Test methods for coverage methods taking include and omit.""" def filenames_in(self, summary, filenames): @@ -461,13 +555,20 @@ class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): summary[k[:-3]] = v return summary - def test_source_package(self): + def test_source_package_as_dir(self): + # pkg1 is a directory, since we cd'd into tests/modules in setUp. lines = self.coverage_usepkgs(source=["pkg1"]) self.filenames_in(lines, "p1a p1b") self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") # Because source= was specified, we do search for unexecuted files. self.assertEqual(lines['p1c'], 0) + def test_source_package_as_package(self): + lines = self.coverage_usepkgs(source=["pkg1.sub"]) + self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") + # Because source= was specified, we do search for unexecuted files. + self.assertEqual(lines['runmod3'], 0) + def test_source_package_dotted(self): lines = self.coverage_usepkgs(source=["pkg1.p1b"]) self.filenames_in(lines, "p1b") diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 5ea2fe1..7df623b 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -143,10 +143,17 @@ class SimpleArcTest(CoverageTest): ) def test_what_is_the_sound_of_no_lines_clapping(self): + if env.JYTHON: + # Jython reports no lines for an empty file. + arcz_missing=".1 1." # pragma: only jython + else: + # Other Pythons report one line. + arcz_missing="" self.check_coverage("""\ # __init__.py """, arcz=".1 1.", + arcz_missing=arcz_missing, ) @@ -252,12 +259,12 @@ class LoopArcTest(CoverageTest): """, arcz=".1 12 23 34 45 36 63 57 7.", ) - # With "while True", 2.x thinks it's computation, 3.x thinks it's - # constant. + # With "while True", 2.x thinks it's computation, + # 3.x thinks it's constant. if env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." else: - arcz = ".1 12 23 27 34 45 36 62 57 7." + arcz = ".1 12 23 34 45 36 62 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -270,6 +277,37 @@ class LoopArcTest(CoverageTest): arcz=arcz, ) + def test_zero_coverage_while_loop(self): + # https://bitbucket.org/ned/coveragepy/issue/502 + self.make_file("main.py", "print('done')") + self.make_file("zero.py", """\ + def method(self): + while True: + return 1 + """) + out = self.run_command("coverage run --branch --source=. main.py") + self.assertEqual(out, 'done\n') + report = self.report_from_command("coverage report -m") + squeezed = self.squeezed_lines(report) + self.assertIn("zero.py 3 3 0 0 0% 1-3", squeezed[3]) + + def test_bug_496_continue_in_constant_while(self): + # https://bitbucket.org/ned/coveragepy/issue/496 + if env.PY3: + arcz = ".1 12 23 34 45 53 46 6." + else: + arcz = ".1 12 23 34 45 52 46 6." + self.check_coverage("""\ + up = iter('ta') + while True: + char = next(up) + if char == 't': + continue + break + """, + arcz=arcz + ) + def test_for_if_else_for(self): self.check_coverage("""\ def branches_2(l): @@ -370,7 +408,7 @@ class LoopArcTest(CoverageTest): def test_other_comprehensions(self): if env.PYVERSION < (2, 7): - self.skipTest("Don't have set or dict comprehensions before 2.7") + self.skipTest("No set or dict comprehensions before 2.7") # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -394,7 +432,7 @@ class LoopArcTest(CoverageTest): def test_multiline_dict_comp(self): if env.PYVERSION < (2, 7): - self.skipTest("Don't have set or dict comprehensions before 2.7") + self.skipTest("No set or dict comprehensions before 2.7") if env.PYVERSION < (3, 5): arcz = "-42 2B B-4 2-4" else: @@ -762,16 +800,19 @@ class ExceptionArcTest(CoverageTest): def test_return_finally(self): self.check_coverage("""\ a = [1] - def func(): - try: - return 10 - finally: - a.append(6) - - assert func() == 10 - assert a == [1, 6] - """, - arcz=".1 12 28 89 9. -23 34 46 6-2", + def check_token(data): + if data: + try: + return 5 + finally: + a.append(7) + return 8 + assert check_token(False) == 8 + assert a == [1] + assert check_token(True) == 5 + assert a == [1, 7] + """, + arcz=".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2", ) def test_except_jump_finally(self): @@ -998,6 +1039,103 @@ class YieldTest(CoverageTest): ) +class OptimizedIfTest(CoverageTest): + """Tests of if statements being optimized away.""" + + def test_optimized_away_if_0(self): + self.check_coverage("""\ + a = 1 + if len([2]): + c = 3 + if 0: # this line isn't in the compiled code. + if len([5]): + d = 6 + else: + e = 8 + f = 9 + """, + lines=[1, 2, 3, 8, 9], + arcz=".1 12 23 28 38 89 9.", + arcz_missing="28", + ) + + def test_optimized_away_if_1(self): + self.check_coverage("""\ + a = 1 + if len([2]): + c = 3 + if 1: # this line isn't in the compiled code, + if len([5]): # but these are. + d = 6 + else: + e = 8 + f = 9 + """, + lines=[1, 2, 3, 5, 6, 9], + arcz=".1 12 23 25 35 56 69 59 9.", + arcz_missing="25 59", + ) + self.check_coverage("""\ + a = 1 + if 1: + b = 3 + c = 4 + d = 5 + """, + lines=[1, 3, 4, 5], + arcz=".1 13 34 45 5.", + ) + + def test_optimized_nested(self): + self.check_coverage("""\ + a = 1 + if 0: + if 0: + b = 4 + else: + c = 6 + else: + if 0: + d = 9 + else: + if 0: e = 11 + f = 12 + if 0: g = 13 + h = 14 + i = 15 + """, + lines=[1, 12, 14, 15], + arcz=".1 1C CE EF F.", + ) + + def test_constant_if(self): + if env.PYPY: + self.skipTest("PyPy doesn't optimize away 'if __debug__:'") + # CPython optimizes away "if __debug__:" + self.check_coverage("""\ + for value in [True, False]: + if value: + if __debug__: + x = 4 + else: + x = 6 + """, + arcz=".1 12 24 41 26 61 1.", + ) + # No Python optimizes away "if not __debug__:" + self.check_coverage("""\ + for value in [True, False]: + if value: + if not __debug__: + x = 4 + else: + x = 6 + """, + arcz=".1 12 23 31 34 41 26 61 1.", + arcz_missing="34 41", + ) + + class MiscArcTest(CoverageTest): """Miscellaneous arc-measuring tests.""" @@ -1067,6 +1205,9 @@ class MiscArcTest(CoverageTest): ) def test_pathologically_long_code_object(self): + if env.JYTHON: + self.skipTest("Bytecode concerns are irrelevant on Jython") + # https://bitbucket.org/ned/coveragepy/issue/359 # The structure of this file is such that an EXTENDED_ARG bytecode is # needed to encode the jump at the end. We weren't interpreting those @@ -1090,21 +1231,6 @@ class MiscArcTest(CoverageTest): arcs_missing=[], arcs_unpredicted=[], ) - def test_optimized_away_lines(self): - self.check_coverage("""\ - a = 1 - if len([2]): - c = 3 - if 0: # this line isn't in the compiled code. - if len([5]): - d = 6 - e = 7 - """, - lines=[1, 2, 3, 7], - arcz=".1 12 23 27 37 7.", - arcz_missing="27", - ) - def test_partial_generators(self): # https://bitbucket.org/ned/coveragepy/issues/475/generator-expression-is-marked-as-not # Line 2 is executed completely. @@ -1340,7 +1466,7 @@ class AsyncTest(CoverageTest): def __init__(self, obj): # 4 self._it = iter(obj) - async def __aiter__(self): # 7 + def __aiter__(self): # 7 return self async def __anext__(self): # A diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3b982eb..2378887 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -5,11 +5,11 @@ import pprint import re -import shlex import sys import textwrap import mock +import pytest import coverage import coverage.cmdline @@ -18,7 +18,7 @@ from coverage.config import CoverageConfig from coverage.data import CoverageData, CoverageDataFiles from coverage.misc import ExceptionDuringRun -from tests.coveragetest import CoverageTest, OK, ERR +from tests.coveragetest import CoverageTest, OK, ERR, command_line class BaseCmdLineTest(CoverageTest): @@ -29,7 +29,7 @@ class BaseCmdLineTest(CoverageTest): # Make a dict mapping function names to the default values that cmdline.py # uses when calling the function. defaults = mock.Mock() - defaults.coverage( + defaults.Coverage( cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None, debug=None, concurrency=None, @@ -39,7 +39,7 @@ class BaseCmdLineTest(CoverageTest): ) defaults.html_report( directory=None, ignore_errors=None, include=None, omit=None, morfs=[], - title=None, + skip_covered=None, title=None ) defaults.report( ignore_errors=None, include=None, omit=None, morfs=[], @@ -54,9 +54,9 @@ class BaseCmdLineTest(CoverageTest): def model_object(self): """Return a Mock suitable for use in CoverageScript.""" mk = mock.Mock() - # We'll invoke .coverage as the constructor, and then keep using the + # We'll invoke .Coverage as the constructor, and then keep using the # same object as the resulting coverage object. - mk.coverage.return_value = mk + mk.Coverage.return_value = mk # The mock needs to get options, but shouldn't need to set them. config = CoverageConfig() @@ -73,11 +73,12 @@ class BaseCmdLineTest(CoverageTest): m = self.model_object() m.path_exists.return_value = path_exists - ret = coverage.cmdline.CoverageScript( + ret = command_line( + args, _covpkg=m, _run_python_file=m.run_python_file, _run_python_module=m.run_python_module, _help_fn=m.help_fn, _path_exists=m.path_exists, - ).command_line(shlex.split(args)) + ) return m, ret @@ -98,7 +99,7 @@ class BaseCmdLineTest(CoverageTest): # calls them with many. But most of them are just the defaults, which # we don't want to have to repeat in all tests. For each call, apply # the defaults. This lets the tests just mention the interesting ones. - for name, args, kwargs in m2.method_calls: + for name, _, kwargs in m2.method_calls: for k, v in self.DEFAULT_KWARGS.get(name, {}).items(): if k not in kwargs: kwargs[k] = v @@ -151,37 +152,37 @@ class CmdLineTest(BaseCmdLineTest): def test_annotate(self): # coverage annotate [-d DIR] [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("annotate", """\ - .coverage() + .Coverage() .load() .annotate() """) self.cmd_executes("annotate -d dir1", """\ - .coverage() + .Coverage() .load() .annotate(directory="dir1") """) self.cmd_executes("annotate -i", """\ - .coverage() + .Coverage() .load() .annotate(ignore_errors=True) """) self.cmd_executes("annotate --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .annotate(omit=["fooey"]) """) self.cmd_executes("annotate --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .annotate(omit=["fooey", "booey"]) """) self.cmd_executes("annotate mod1", """\ - .coverage() + .Coverage() .load() .annotate(morfs=["mod1"]) """) self.cmd_executes("annotate mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .annotate(morfs=["mod1", "mod2", "mod3"]) """) @@ -189,20 +190,20 @@ class CmdLineTest(BaseCmdLineTest): def test_combine(self): # coverage combine with args self.cmd_executes("combine datadir1", """\ - .coverage() + .Coverage() .combine(["datadir1"], strict=True) .save() """) # coverage combine, appending self.cmd_executes("combine --append datadir1", """\ - .coverage() + .Coverage() .load() .combine(["datadir1"], strict=True) .save() """) # coverage combine without args self.cmd_executes("combine", """\ - .coverage() + .Coverage() .combine(None, strict=True) .save() """) @@ -210,12 +211,12 @@ class CmdLineTest(BaseCmdLineTest): def test_combine_doesnt_confuse_options_with_args(self): # https://bitbucket.org/ned/coveragepy/issues/385/coverage-combine-doesnt-work-with-rcfile self.cmd_executes("combine --rcfile cov.ini", """\ - .coverage(config_file='cov.ini') + .Coverage(config_file='cov.ini') .combine(None, strict=True) .save() """) self.cmd_executes("combine --rcfile cov.ini data1 data2/more", """\ - .coverage(config_file='cov.ini') + .Coverage(config_file='cov.ini') .combine(["data1", "data2/more"], strict=True) .save() """) @@ -239,7 +240,7 @@ class CmdLineTest(BaseCmdLineTest): def test_erase(self): # coverage erase self.cmd_executes("erase", """\ - .coverage() + .Coverage() .erase() """) @@ -262,42 +263,42 @@ class CmdLineTest(BaseCmdLineTest): def test_html(self): # coverage html -d DIR [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("html", """\ - .coverage() + .Coverage() .load() .html_report() """) self.cmd_executes("html -d dir1", """\ - .coverage() + .Coverage() .load() .html_report(directory="dir1") """) self.cmd_executes("html -i", """\ - .coverage() + .Coverage() .load() .html_report(ignore_errors=True) """) self.cmd_executes("html --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .html_report(omit=["fooey"]) """) self.cmd_executes("html --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .html_report(omit=["fooey", "booey"]) """) self.cmd_executes("html mod1", """\ - .coverage() + .Coverage() .load() .html_report(morfs=["mod1"]) """) self.cmd_executes("html mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .html_report(morfs=["mod1", "mod2", "mod3"]) """) self.cmd_executes("html --title=Hello_there", """\ - .coverage() + .Coverage() .load() .html_report(title='Hello_there') """) @@ -305,42 +306,42 @@ class CmdLineTest(BaseCmdLineTest): def test_report(self): # coverage report [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] self.cmd_executes("report", """\ - .coverage() + .Coverage() .load() .report(show_missing=None) """) self.cmd_executes("report -i", """\ - .coverage() + .Coverage() .load() .report(ignore_errors=True) """) self.cmd_executes("report -m", """\ - .coverage() + .Coverage() .load() .report(show_missing=True) """) self.cmd_executes("report --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .report(omit=["fooey"]) """) self.cmd_executes("report --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .report(omit=["fooey", "booey"]) """) self.cmd_executes("report mod1", """\ - .coverage() + .Coverage() .load() .report(morfs=["mod1"]) """) self.cmd_executes("report mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .report(morfs=["mod1", "mod2", "mod3"]) """) self.cmd_executes("report --skip-covered", """\ - .coverage() + .Coverage() .load() .report(skip_covered=True) """) @@ -350,7 +351,7 @@ class CmdLineTest(BaseCmdLineTest): # run calls coverage.erase first. self.cmd_executes("run foo.py", """\ - .coverage() + .Coverage() .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -359,7 +360,7 @@ class CmdLineTest(BaseCmdLineTest): """) # run -a combines with an existing data file before saving. self.cmd_executes("run -a foo.py", """\ - .coverage() + .Coverage() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -369,7 +370,7 @@ class CmdLineTest(BaseCmdLineTest): """, path_exists=True) # run -a doesn't combine anything if the data file doesn't exist. self.cmd_executes("run -a foo.py", """\ - .coverage() + .Coverage() .start() .run_python_file('foo.py', ['foo.py']) .stop() @@ -378,7 +379,7 @@ class CmdLineTest(BaseCmdLineTest): """, path_exists=False) # --timid sets a flag, and program arguments get passed through. self.cmd_executes("run --timid foo.py abc 123", """\ - .coverage(timid=True) + .Coverage(timid=True) .erase() .start() .run_python_file('foo.py', ['foo.py', 'abc', '123']) @@ -387,7 +388,7 @@ class CmdLineTest(BaseCmdLineTest): """) # -L sets a flag, and flags for the program don't confuse us. self.cmd_executes("run -p -L foo.py -a -b", """\ - .coverage(cover_pylib=True, data_suffix=True) + .Coverage(cover_pylib=True, data_suffix=True) .erase() .start() .run_python_file('foo.py', ['foo.py', '-a', '-b']) @@ -395,7 +396,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --branch foo.py", """\ - .coverage(branch=True) + .Coverage(branch=True) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -403,7 +404,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --rcfile=myrc.rc foo.py", """\ - .coverage(config_file="myrc.rc") + .Coverage(config_file="myrc.rc") .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -411,7 +412,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --include=pre1,pre2 foo.py", """\ - .coverage(include=["pre1", "pre2"]) + .Coverage(include=["pre1", "pre2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -419,7 +420,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --omit=opre1,opre2 foo.py", """\ - .coverage(omit=["opre1", "opre2"]) + .Coverage(omit=["opre1", "opre2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -427,7 +428,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --include=pre1,pre2 --omit=opre1,opre2 foo.py", """\ - .coverage(include=["pre1", "pre2"], omit=["opre1", "opre2"]) + .Coverage(include=["pre1", "pre2"], omit=["opre1", "opre2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -435,7 +436,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --source=quux,hi.there,/home/bar foo.py", """\ - .coverage(source=["quux", "hi.there", "/home/bar"]) + .Coverage(source=["quux", "hi.there", "/home/bar"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -443,7 +444,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --concurrency=gevent foo.py", """\ - .coverage(concurrency='gevent') + .Coverage(concurrency='gevent') .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -451,7 +452,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --concurrency=multiprocessing foo.py", """\ - .coverage(concurrency='multiprocessing') + .Coverage(concurrency='multiprocessing') .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -461,16 +462,16 @@ class CmdLineTest(BaseCmdLineTest): def test_bad_concurrency(self): self.command_line("run --concurrency=nothing", ret=ERR) - out = self.stdout() - self.assertIn("option --concurrency: invalid choice: 'nothing'", out) + err = self.stderr() + self.assertIn("option --concurrency: invalid choice: 'nothing'", err) def test_no_multiple_concurrency(self): # You can't use multiple concurrency values on the command line. # I would like to have a better message about not allowing multiple # values for this option, but optparse is not that flexible. self.command_line("run --concurrency=multiprocessing,gevent foo.py", ret=ERR) - out = self.stdout() - self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", out) + err = self.stderr() + self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", err) def test_multiprocessing_needs_config_file(self): # You can't use command-line args to add options to multiprocessing @@ -479,12 +480,12 @@ class CmdLineTest(BaseCmdLineTest): self.command_line("run --concurrency=multiprocessing --branch foo.py", ret=ERR) self.assertIn( "Options affecting multiprocessing must be specified in a configuration file.", - self.stdout() + self.stderr() ) def test_run_debug(self): self.cmd_executes("run --debug=opt1 foo.py", """\ - .coverage(debug=["opt1"]) + .Coverage(debug=["opt1"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -492,7 +493,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --debug=opt1,opt2 foo.py", """\ - .coverage(debug=["opt1","opt2"]) + .Coverage(debug=["opt1","opt2"]) .erase() .start() .run_python_file('foo.py', ['foo.py']) @@ -502,7 +503,7 @@ class CmdLineTest(BaseCmdLineTest): def test_run_module(self): self.cmd_executes("run -m mymodule", """\ - .coverage() + .Coverage() .erase() .start() .run_python_module('mymodule', ['mymodule']) @@ -510,7 +511,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run -m mymodule -qq arg1 arg2", """\ - .coverage() + .Coverage() .erase() .start() .run_python_module('mymodule', ['mymodule', '-qq', 'arg1', 'arg2']) @@ -518,7 +519,7 @@ class CmdLineTest(BaseCmdLineTest): .save() """) self.cmd_executes("run --branch -m mymodule", """\ - .coverage(branch=True) + .Coverage(branch=True) .erase() .start() .run_python_module('mymodule', ['mymodule']) @@ -529,51 +530,51 @@ class CmdLineTest(BaseCmdLineTest): def test_run_nothing(self): self.command_line("run", ret=ERR) - self.assertIn("Nothing to do", self.stdout()) + self.assertIn("Nothing to do", self.stderr()) def test_cant_append_parallel(self): self.command_line("run --append --parallel-mode foo.py", ret=ERR) - self.assertIn("Can't append to data files in parallel mode.", self.stdout()) + self.assertIn("Can't append to data files in parallel mode.", self.stderr()) def test_xml(self): # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] self.cmd_executes("xml", """\ - .coverage() + .Coverage() .load() .xml_report() """) self.cmd_executes("xml -i", """\ - .coverage() + .Coverage() .load() .xml_report(ignore_errors=True) """) self.cmd_executes("xml -o myxml.foo", """\ - .coverage() + .Coverage() .load() .xml_report(outfile="myxml.foo") """) self.cmd_executes("xml -o -", """\ - .coverage() + .Coverage() .load() .xml_report(outfile="-") """) self.cmd_executes("xml --omit fooey", """\ - .coverage(omit=["fooey"]) + .Coverage(omit=["fooey"]) .load() .xml_report(omit=["fooey"]) """) self.cmd_executes("xml --omit fooey,booey", """\ - .coverage(omit=["fooey", "booey"]) + .Coverage(omit=["fooey", "booey"]) .load() .xml_report(omit=["fooey", "booey"]) """) self.cmd_executes("xml mod1", """\ - .coverage() + .Coverage() .load() .xml_report(morfs=["mod1"]) """) self.cmd_executes("xml mod1 mod2 mod3", """\ - .coverage() + .Coverage() .load() .xml_report(morfs=["mod1", "mod2", "mod3"]) """) @@ -661,9 +662,9 @@ class CmdLineStdoutTest(BaseCmdLineTest): def test_error(self): self.command_line("fooey kablooey", ret=ERR) - out = self.stdout() - self.assertIn("fooey", out) - self.assertIn("help", out) + err = self.stderr() + self.assertIn("fooey", err) + self.assertIn("help", err) class CmdMainTest(CoverageTest): @@ -693,13 +694,9 @@ class CmdMainTest(CoverageTest): def setUp(self): super(CmdMainTest, self).setUp() - self.old_CoverageScript = coverage.cmdline.CoverageScript + old_CoverageScript = coverage.cmdline.CoverageScript coverage.cmdline.CoverageScript = self.CoverageScriptStub - self.addCleanup(self.cleanup_coverage_script) - - def cleanup_coverage_script(self): - """Restore CoverageScript when the test is done.""" - coverage.cmdline.CoverageScript = self.old_CoverageScript + self.addCleanup(setattr, coverage.cmdline, 'CoverageScript', old_CoverageScript) def test_normal(self): ret = coverage.cmdline.main(['hello']) @@ -722,3 +719,59 @@ class CmdMainTest(CoverageTest): def test_exit(self): ret = coverage.cmdline.main(['exit']) self.assertEqual(ret, 23) + + +class CoverageReportingFake(object): + """A fake Coverage and Coverage.coverage test double.""" + # pylint: disable=missing-docstring + def __init__(self, report_result, html_result, xml_result): + self.report_result = report_result + self.html_result = html_result + self.xml_result = xml_result + + def Coverage(self, *args_unused, **kwargs_unused): + return self + + def set_option(self, optname, optvalue): + setattr(self, optname, optvalue) + + def get_option(self, optname): + return getattr(self, optname) + + def load(self): + pass + + def report(self, *args_unused, **kwargs_unused): + return self.report_result + + def html_report(self, *args_unused, **kwargs_unused): + return self.html_result + + def xml_report(self, *args_unused, **kwargs_unused): + return self.xml_result + + +@pytest.mark.parametrize("results, fail_under, cmd, ret", [ + # Command-line switch properly checks the result of reporting functions. + ((20, 30, 40), None, "report --fail-under=19", 0), + ((20, 30, 40), None, "report --fail-under=21", 2), + ((20, 30, 40), None, "html --fail-under=29", 0), + ((20, 30, 40), None, "html --fail-under=31", 2), + ((20, 30, 40), None, "xml --fail-under=39", 0), + ((20, 30, 40), None, "xml --fail-under=41", 2), + # Configuration file setting properly checks the result of reporting. + ((20, 30, 40), 19, "report", 0), + ((20, 30, 40), 21, "report", 2), + ((20, 30, 40), 29, "html", 0), + ((20, 30, 40), 31, "html", 2), + ((20, 30, 40), 39, "xml", 0), + ((20, 30, 40), 41, "xml", 2), + # Command-line overrides configuration. + ((20, 30, 40), 19, "report --fail-under=21", 2), +]) +def test_fail_under(results, fail_under, cmd, ret): + cov = CoverageReportingFake(*results) + if fail_under: + cov.set_option("report:fail_under", fail_under) + ret_actual = command_line(cmd, _covpkg=cov) + assert ret_actual == ret diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index e36db30..841b5df 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -3,9 +3,10 @@ """Tests for concurrency libraries.""" -import multiprocessing import threading +from flaky import flaky + import coverage from coverage import env from coverage.files import abs_file @@ -16,6 +17,11 @@ from tests.coveragetest import CoverageTest # These libraries aren't always available, we'll skip tests if they aren't. try: + import multiprocessing +except ImportError: # pragma: only jython + multiprocessing = None + +try: import eventlet except ImportError: eventlet = None @@ -25,7 +31,10 @@ try: except ImportError: gevent = None -import greenlet +try: + import greenlet +except ImportError: # pragma: only jython + greenlet = None def measurable_line(l): @@ -40,6 +49,9 @@ def measurable_line(l): return False if l.startswith('else:'): return False + if env.JYTHON and l.startswith(('try:', 'except:', 'except ', 'break', 'with ')): + # Jython doesn't measure these statements. + return False # pragma: only jython return True @@ -342,9 +354,15 @@ MULTI_CODE = """ """ +@flaky # Sometimes a test fails due to inherent randomness. Try one more time. class MultiprocessingTest(CoverageTest): """Test support of the multiprocessing module.""" + def setUp(self): + super(MultiprocessingTest, self).setUp() + if not multiprocessing: + self.skipTest("No multiprocessing in this Python") # pragma: only jython + def try_multiprocessing_code( self, code, expected_out, the_module, concurrency="multiprocessing" ): @@ -353,6 +371,7 @@ class MultiprocessingTest(CoverageTest): self.make_file(".coveragerc", """\ [run] concurrency = %s + source = . """ % concurrency) if env.PYVERSION >= (3, 4): diff --git a/tests/test_config.py b/tests/test_config.py index cf8a6a7..9224046 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -95,6 +95,15 @@ class ConfigTest(CoverageTest): cov = coverage.Coverage(data_file="fromarg.dat") self.assertEqual(cov.config.data_file, "fromarg.dat") + def test_debug_from_environment(self): + self.make_file(".coveragerc", """\ + [run] + debug = dataio, pids + """) + self.set_environ("COVERAGE_DEBUG", "callers, fooey") + cov = coverage.Coverage() + self.assertEqual(cov.config.debug, ["dataio", "pids", "callers", "fooey"]) + def test_parse_errors(self): # Im-parsable values raise CoverageException, with details. bad_configs_and_msgs = [ @@ -206,7 +215,7 @@ class ConfigTest(CoverageTest): [coverage:run] huh = what? """) - msg = r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg" + msg = (r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg") with self.assertRaisesRegex(CoverageException, msg): _ = coverage.Coverage() @@ -230,12 +239,13 @@ class ConfigFileTest(CoverageTest): branch = 1 cover_pylib = TRUE parallel = on - include = a/ , b/ concurrency = thread source = myapp plugins = plugins.a_plugin plugins.another + debug = callers, pids , dataio + disable_warnings = abcd , efgh [{section}report] ; these settings affect reporting. @@ -294,19 +304,32 @@ class ConfigFileTest(CoverageTest): examples/ """ + # Just some sample tox.ini text from the docs. + TOX_INI = """\ + [tox] + envlist = py{26,27,33,34,35}-{c,py}tracer + skip_missing_interpreters = True + + [testenv] + commands = + # Create tests/zipmods.zip, install the egg1 egg + python igor.py zip_mods install_egg + """ + def assert_config_settings_are_correct(self, cov): """Check that `cov` has all the settings from LOTSA_SETTINGS.""" self.assertTrue(cov.config.timid) self.assertEqual(cov.config.data_file, "something_or_other.dat") self.assertTrue(cov.config.branch) self.assertTrue(cov.config.cover_pylib) + self.assertEqual(cov.config.debug, ["callers", "pids", "dataio"]) self.assertTrue(cov.config.parallel) self.assertEqual(cov.config.concurrency, ["thread"]) self.assertEqual(cov.config.source, ["myapp"]) + self.assertEqual(cov.config.disable_warnings, ["abcd", "efgh"]) self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"]) self.assertTrue(cov.config.ignore_errors) - self.assertEqual(cov.config.include, ["a/", "b/"]) self.assertEqual(cov.config.omit, ["one", "another", "some_more", "yet_more"]) self.assertEqual(cov.config.precision, 3) @@ -338,29 +361,39 @@ class ConfigFileTest(CoverageTest): cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def test_config_file_settings_in_setupcfg(self): - # Configuration will be read from setup.cfg from sections prefixed with - # "coverage:" + def check_config_file_settings_in_other_file(self, fname, contents): + """Check config will be read from another file, with prefixed sections.""" nested = self.LOTSA_SETTINGS.format(section="coverage:") - self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG) + fname = self.make_file(fname, nested + "\n" + contents) cov = coverage.Coverage() self.assert_config_settings_are_correct(cov) - def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self): - # Configuration will be read from setup.cfg from sections prefixed with - # "coverage:", even if the API said to read from a (non-existent) - # .coveragerc file. + def test_config_file_settings_in_setupcfg(self): + self.check_config_file_settings_in_other_file("setup.cfg", self.SETUP_CFG) + + def test_config_file_settings_in_toxini(self): + self.check_config_file_settings_in_other_file("tox.ini", self.TOX_INI) + + def check_other_config_if_coveragerc_specified(self, fname, contents): + """Check that config `fname` is read if .coveragerc is missing, but specified.""" nested = self.LOTSA_SETTINGS.format(section="coverage:") - self.make_file("setup.cfg", nested + "\n" + self.SETUP_CFG) + self.make_file(fname, nested + "\n" + contents) cov = coverage.Coverage(config_file=".coveragerc") self.assert_config_settings_are_correct(cov) - def test_setupcfg_only_if_not_coveragerc(self): + def test_config_file_settings_in_setupcfg_if_coveragerc_specified(self): + self.check_other_config_if_coveragerc_specified("setup.cfg", self.SETUP_CFG) + + def test_config_file_settings_in_tox_if_coveragerc_specified(self): + self.check_other_config_if_coveragerc_specified("tox.ini", self.TOX_INI) + + def check_other_not_read_if_coveragerc(self, fname): + """Check config `fname` is not read if .coveragerc exists.""" self.make_file(".coveragerc", """\ [run] include = foo """) - self.make_file("setup.cfg", """\ + self.make_file(fname, """\ [coverage:run] omit = bar branch = true @@ -370,8 +403,15 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.omit, None) self.assertEqual(cov.config.branch, False) - def test_setupcfg_only_if_prefixed(self): - self.make_file("setup.cfg", """\ + def test_setupcfg_only_if_not_coveragerc(self): + self.check_other_not_read_if_coveragerc("setup.cfg") + + def test_toxini_only_if_not_coveragerc(self): + self.check_other_not_read_if_coveragerc("tox.ini") + + def check_other_config_need_prefixes(self, fname): + """Check that `fname` sections won't be read if un-prefixed.""" + self.make_file(fname, """\ [run] omit = bar branch = true @@ -380,6 +420,21 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.config.omit, None) self.assertEqual(cov.config.branch, False) + def test_setupcfg_only_if_prefixed(self): + self.check_other_config_need_prefixes("setup.cfg") + + def test_toxini_only_if_prefixed(self): + self.check_other_config_need_prefixes("tox.ini") + + def test_tox_ini_even_if_setup_cfg(self): + # There's a setup.cfg, but no coverage settings in it, so tox.ini + # is read. + nested = self.LOTSA_SETTINGS.format(section="coverage:") + self.make_file("tox.ini", self.TOX_INI + "\n" + nested) + self.make_file("setup.cfg", self.SETUP_CFG) + cov = coverage.Coverage() + self.assert_config_settings_are_correct(cov) + def test_non_ascii(self): self.make_file(".coveragerc", """\ [report] diff --git a/tests/test_coverage.py b/tests/test_coverage.py index a52aced..bda61fc 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -418,7 +418,7 @@ class SimpleStatementTest(CoverageTest): """, [1,2,3,4,5], "4") - def test_strange_unexecuted_continue(self): + def test_strange_unexecuted_continue(self): # pragma: not covered # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different # versions of Python, so don't run this test: @@ -567,7 +567,7 @@ class SimpleStatementTest(CoverageTest): def test_nonascii(self): self.check_coverage("""\ - # coding: utf8 + # coding: utf-8 a = 2 b = 3 """, diff --git a/tests/test_data.py b/tests/test_data.py index 4bccdcf..46999f6 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -13,10 +13,11 @@ import mock from coverage.backward import StringIO from coverage.data import CoverageData, CoverageDataFiles, debug_main, canonicalize_json_data +from coverage.debug import DebugControlString from coverage.files import PathAliases, canonical_filename from coverage.misc import CoverageException -from tests.coveragetest import CoverageTest, DebugControlString +from tests.coveragetest import CoverageTest LINES_1 = { @@ -554,9 +555,7 @@ class CoverageDataFilesTest(DataTestHelpers, CoverageTest): self.assertRegex( debug.get_output(), - r"^Creating CoverageData object\n" - r"Writing data to '.*\.coverage'\n" - r"Creating CoverageData object\n" + r"^Writing data to '.*\.coverage'\n" r"Reading data from '.*\.coverage'\n$" ) diff --git a/tests/test_debug.py b/tests/test_debug.py index 2d553ee..f733d72 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -4,13 +4,15 @@ """Tests of coverage/debug.py""" import os -import re + +import pytest import coverage from coverage.backward import StringIO -from coverage.debug import info_formatter, info_header, short_stack +from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack from tests.coveragetest import CoverageTest +from tests.helpers import re_lines class InfoFormatterTest(CoverageTest): @@ -39,15 +41,34 @@ class InfoFormatterTest(CoverageTest): lines = list(info_formatter(('info%d' % i, i) for i in range(3))) self.assertEqual(lines, ['info0: 0', 'info1: 1', 'info2: 2']) - def test_info_header(self): - self.assertEqual( - info_header("x"), - "-- x ---------------------------------------------------------" - ) - self.assertEqual( - info_header("hello there"), - "-- hello there -----------------------------------------------" - ) + +@pytest.mark.parametrize("label, header", [ + ("x", "-- x ---------------------------------------------------------"), + ("hello there", "-- hello there -----------------------------------------------"), +]) +def test_info_header(label, header): + assert info_header(label) == header + + +@pytest.mark.parametrize("id64, id16", [ + (0x1234, 0x1234), + (0x12340000, 0x1234), + (0xA5A55A5A, 0xFFFF), + (0x1234cba956780fed, 0x8008), +]) +def test_short_id(id64, id16): + assert short_id(id64) == id16 + + +@pytest.mark.parametrize("text, filters, result", [ + ("hello", [], "hello"), + ("hello\n", [], "hello\n"), + ("hello\nhello\n", [], "hello\nhello\n"), + ("hello\nbye\n", [lambda x: "="+x], "=hello\n=bye\n"), + ("hello\nbye\n", [lambda x: "="+x, lambda x: x+"\ndone\n"], "=hello\ndone\n=bye\ndone\n"), +]) +def test_filter_text(text, filters, result): + assert filter_text(text, filters) == result class DebugTraceTest(CoverageTest): @@ -70,7 +91,7 @@ class DebugTraceTest(CoverageTest): self.start_import_stop(cov, "f1") cov.save() - out_lines = debug_out.getvalue().splitlines() + out_lines = debug_out.getvalue() return out_lines def test_debug_no_trace(self): @@ -86,7 +107,7 @@ class DebugTraceTest(CoverageTest): self.assertIn("Tracing 'f1.py'", out_lines) # We should have lines like "Not tracing 'collector.py'..." - coverage_lines = lines_matching( + coverage_lines = re_lines( out_lines, r"^Not tracing .*: is part of coverage.py$" ) @@ -96,28 +117,29 @@ class DebugTraceTest(CoverageTest): out_lines = self.f1_debug_output(["trace", "pid"]) # Now our lines are always prefixed with the process id. - pid_prefix = "^pid %5d: " % os.getpid() - pid_lines = lines_matching(out_lines, pid_prefix) + pid_prefix = r"^%5d\.[0-9a-f]{4}: " % os.getpid() + pid_lines = re_lines(out_lines, pid_prefix) self.assertEqual(pid_lines, out_lines) # We still have some tracing, and some not tracing. - self.assertTrue(lines_matching(out_lines, pid_prefix + "Tracing ")) - self.assertTrue(lines_matching(out_lines, pid_prefix + "Not tracing ")) + self.assertTrue(re_lines(out_lines, pid_prefix + "Tracing ")) + self.assertTrue(re_lines(out_lines, pid_prefix + "Not tracing ")) def test_debug_callers(self): out_lines = self.f1_debug_output(["pid", "dataop", "dataio", "callers"]) - print("\n".join(out_lines)) + print(out_lines) # For every real message, there should be a stack # trace with a line like "f1_debug_output : /Users/ned/coverage/tests/test_debug.py @71" - real_messages = lines_matching(out_lines, r"^pid\s+\d+: ") + real_messages = re_lines(out_lines, r" @\d+", match=False).splitlines() frame_pattern = r"\s+f1_debug_output : .*tests[/\\]test_debug.py @\d+$" - frames = lines_matching(out_lines, frame_pattern) + frames = re_lines(out_lines, frame_pattern).splitlines() self.assertEqual(len(real_messages), len(frames)) # The last message should be "Writing data", and the last frame should # be write_file in data.py. - self.assertRegex(real_messages[-1], r"^pid\s+\d+: Writing data") - self.assertRegex(out_lines[-1], r"\s+write_file : .*coverage[/\\]data.py @\d+$") + self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Writing data") + last_line = out_lines.splitlines()[-1] + self.assertRegex(last_line, r"\s+write_file : .*coverage[/\\]data.py @\d+$") def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) @@ -130,20 +152,23 @@ class DebugTraceTest(CoverageTest): """.split() for label in labels: label_pat = r"^\s*%s: " % label - self.assertEqual(len(lines_matching(out_lines, label_pat)), 1) + self.assertEqual( + len(re_lines(out_lines, label_pat).splitlines()), + 1 + ) def test_debug_sys(self): out_lines = self.f1_debug_output(["sys"]) labels = """ - version coverage cover_dirs pylib_dirs tracer config_files + version coverage cover_paths pylib_paths tracer config_files configs_read data_path python platform implementation executable cwd path environment command_line cover_match pylib_match """.split() for label in labels: label_pat = r"^\s*%s: " % label self.assertEqual( - len(lines_matching(out_lines, label_pat)), + len(re_lines(out_lines, label_pat).splitlines()), 1, msg="Incorrect lines for %r" % label, ) @@ -181,8 +206,3 @@ class ShortStackTest(CoverageTest): def test_short_stack_skip(self): stack = f_one(skip=1).splitlines() self.assertIn("f_two", stack[-1]) - - -def lines_matching(lines, pat): - """Gives the list of lines from `lines` that match `pat`.""" - return [l for l in lines if re.search(pat, l)] diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 889d6cf..bad3da9 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -10,6 +10,7 @@ import os.path import re import sys +from coverage import env from coverage.backward import binary_bytes from coverage.execfile import run_python_file, run_python_module from coverage.misc import NoCode, NoSource @@ -43,7 +44,8 @@ class RunFileTest(CoverageTest): self.assertEqual(mod_globs['__main__.DATA'], "xyzzy") # Argv should have the proper values. - self.assertEqual(mod_globs['argv'], [TRY_EXECFILE, "arg1", "arg2"]) + self.assertEqual(mod_globs['argv0'], TRY_EXECFILE) + self.assertEqual(mod_globs['argv1-n'], ["arg1", "arg2"]) # __builtins__ should have the right values, like open(). self.assertEqual(mod_globs['__builtins__.has_open'], True) @@ -102,6 +104,9 @@ class RunPycFileTest(CoverageTest): def make_pyc(self): """Create a .pyc file, and return the relative path to it.""" + if env.JYTHON: + self.skipTest("Can't make .pyc files on Jython") + self.make_file("compiled.py", """\ def doit(): print("I am here!") @@ -145,6 +150,25 @@ class RunPycFileTest(CoverageTest): with self.assertRaisesRegex(NoCode, "No file to run: 'xyzzy.pyc'"): run_python_file("xyzzy.pyc", []) + def test_running_py_from_binary(self): + # Use make_file to get the bookkeeping. Ideally, it would + # be able to write binary files. + bf = self.make_file("binary") + with open(bf, "wb") as f: + f.write(b'\x7fELF\x02\x01\x01\x00\x00\x00') + + msg = ( + r"Couldn't run 'binary' as Python code: " + r"(TypeError|ValueError): " + r"(" + r"compile\(\) expected string without null bytes" # for py2 + r"|" + r"source code string cannot contain null bytes" # for py3 + r")" + ) + with self.assertRaisesRegex(Exception, msg): + run_python_file(bf, [bf]) + class RunModuleTest(CoverageTest): """Test run_python_module.""" diff --git a/tests/test_farm.py b/tests/test_farm.py index ae9e915..1b52bc2 100644 --- a/tests/test_farm.py +++ b/tests/test_farm.py @@ -1,7 +1,7 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -"""Run tests in the farm sub-directory. Designed for nose.""" +"""Run tests in the farm sub-directory. Designed for pytest.""" import difflib import filecmp @@ -11,22 +11,28 @@ import os import re import shutil import sys -import unittest -from nose.plugins.skip import SkipTest +import pytest -from unittest_mixins import ModuleAwareMixin, SysPathAwareMixin, change_dir, saved_sys_path +from unittest_mixins import ModuleAwareMixin, SysPathAwareMixin, change_dir from tests.helpers import run_command from tests.backtest import execfile # pylint: disable=redefined-builtin +from coverage import env +from coverage.backunittest import unittest from coverage.debug import _TEST_NAME_FILE -def test_farm(clean_only=False): - """A test-generating function for nose to find and run.""" - for fname in glob.glob("tests/farm/*/*.py"): - case = FarmTestCase(fname, clean_only) - yield (case,) +# Look for files that become tests. +TEST_FILES = glob.glob("tests/farm/*/*.py") + + +@pytest.mark.parametrize("filename", TEST_FILES) +def test_farm(filename): + if env.JYTHON: + # All of the farm tests use reporting, so skip them all. + skip("Farm tests don't run on Jython") + FarmTestCase(filename).run_fully() # "rU" was deprecated in 3.4 @@ -51,8 +57,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): cleaning-only, or run and leave the results for debugging). This class is a unittest.TestCase so that we can use behavior-modifying - mixins, but it's only useful as a nose test function. Yes, this is - confusing. + mixins, but it's only useful as a test function. Yes, this is confusing. """ @@ -75,38 +80,38 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): self.ok = True def setUp(self): - """Test set up, run by nose before __call__.""" + """Test set up, run by the test runner before __call__.""" super(FarmTestCase, self).setUp() # Modules should be importable from the current directory. sys.path.insert(0, '') def tearDown(self): - """Test tear down, run by nose after __call__.""" + """Test tear down, run by the test runner after __call__.""" # Make sure the test is cleaned up, unless we never want to, or if the # test failed. - if not self.dont_clean and self.ok: # pragma: part covered + if not self.dont_clean and self.ok: # pragma: part covered self.clean_only = True self() super(FarmTestCase, self).tearDown() - # This object will be run by nose via the __call__ method, and nose - # doesn't do cleanups in that case. Do them now. + # This object will be run via the __call__ method, and test runners + # don't do cleanups in that case. Do them now. self.doCleanups() - def runTest(self): + def runTest(self): # pragma: not covered """Here to make unittest.TestCase happy, but will never be invoked.""" raise Exception("runTest isn't used in this class!") def __call__(self): """Execute the test from the run.py file.""" - if _TEST_NAME_FILE: # pragma: debugging + if _TEST_NAME_FILE: # pragma: debugging with open(_TEST_NAME_FILE, "w") as f: f.write(self.description.replace("/", "_")) # Prepare a dictionary of globals for the run.py files to use. fns = """ - copy run runfunc clean skip + copy run clean skip compare contains contains_any doesnt_contain """.split() if self.clean_only: @@ -114,7 +119,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): glo['clean'] = clean else: glo = dict((fn, globals()[fn]) for fn in fns) - if self.dont_clean: # pragma: not covered + if self.dont_clean: # pragma: debugging glo['clean'] = noop with change_dir(self.dir): @@ -124,7 +129,7 @@ class FarmTestCase(ModuleAwareMixin, SysPathAwareMixin, unittest.TestCase): self.ok = False raise - def run_fully(self): # pragma: not covered + def run_fully(self): """Run as a full test case, with setUp and tearDown.""" self.setUp() try: @@ -142,8 +147,6 @@ def noop(*args_unused, **kwargs_unused): def copy(src, dst): """Copy a directory.""" - if os.path.exists(dst): - shutil.rmtree(dst) shutil.copytree(src, dst) @@ -168,37 +171,15 @@ def run(cmds, rundir="src", outfile=None): if outfile: fout.write(output) if retcode: - raise Exception("command exited abnormally") + raise Exception("command exited abnormally") # pragma: only failure finally: if outfile: fout.close() -def runfunc(fn, rundir="src", addtopath=None): - """Run a function. - - `fn` is a callable. - `rundir` is the directory in which to run the function. - - """ - with change_dir(rundir): - with saved_sys_path(): - if addtopath is not None: - sys.path.insert(0, addtopath) - fn() - - -def compare( - dir1, dir2, file_pattern=None, size_within=0, - left_extra=False, right_extra=False, scrubs=None -): +def compare(dir1, dir2, file_pattern=None, size_within=0, left_extra=False, scrubs=None): """Compare files matching `file_pattern` in `dir1` and `dir2`. - `dir2` is interpreted as a prefix, with Python version numbers appended - to find the actual directory to compare with. "foo" will compare - against "foo_v241", "foo_v24", "foo_v2", or "foo", depending on which - directory is found first. - `size_within` is a percentage delta for the file sizes. If non-zero, then the file contents are not compared (since they are expected to often be different), but the file sizes must be within this amount. @@ -206,8 +187,7 @@ def compare( within 10 percent of each other to compare equal. `left_extra` true means the left directory can have extra files in it - without triggering an assertion. `right_extra` means the right - directory can. + without triggering an assertion. `scrubs` is a list of pairs, regexes to find and literal strings to replace them with to scrub the files of unimportant differences. @@ -216,15 +196,6 @@ def compare( matches. """ - # Search for a dir2 with a version suffix. - version_suff = ''.join(map(str, sys.version_info[:3])) - while version_suff: - trydir = dir2 + '_v' + version_suff - if os.path.exists(trydir): - dir2 = trydir - break - version_suff = version_suff[:-1] - assert os.path.exists(dir1), "Left directory missing: %s" % dir1 assert os.path.exists(dir2), "Right directory missing: %s" % dir2 @@ -248,9 +219,9 @@ def compare( if (big - little) / float(little) > size_within/100.0: # print "%d %d" % (big, little) # print "Left: ---\n%s\n-----\n%s" % (left, right) - wrong_size.append("%s (%s,%s)" % (f, size_l, size_r)) + wrong_size.append("%s (%s,%s)" % (f, size_l, size_r)) # pragma: only failure if wrong_size: - print("File sizes differ between %s and %s: %s" % ( + print("File sizes differ between %s and %s: %s" % ( # pragma: only failure dir1, dir2, ", ".join(wrong_size) )) @@ -270,7 +241,7 @@ def compare( if scrubs: left = scrub(left, scrubs) right = scrub(right, scrubs) - if left != right: + if left != right: # pragma: only failure text_diff.append(f) left = left.splitlines() right = right.splitlines() @@ -279,8 +250,7 @@ def compare( if not left_extra: assert not left_only, "Files in %s only: %s" % (dir1, left_only) - if not right_extra: - assert not right_only, "Files in %s only: %s" % (dir2, right_only) + assert not right_only, "Files in %s only: %s" % (dir2, right_only) def contains(filename, *strlist): @@ -308,7 +278,10 @@ def contains_any(filename, *strlist): for s in strlist: if s in text: return - assert False, "Missing content in %s: %r [1 of %d]" % (filename, strlist[0], len(strlist),) + + assert False, ( # pragma: only failure + "Missing content in %s: %r [1 of %d]" % (filename, strlist[0], len(strlist),) + ) def doesnt_contain(filename, *strlist): @@ -334,7 +307,7 @@ def clean(cleandir): if os.path.exists(cleandir): try: shutil.rmtree(cleandir) - except OSError: # pragma: not covered + except OSError: # pragma: cant happen if tries == 1: raise else: @@ -345,7 +318,7 @@ def clean(cleandir): def skip(msg=None): """Skip the current test.""" - raise SkipTest(msg) + raise unittest.SkipTest(msg) # Helpers @@ -371,12 +344,12 @@ def scrub(strdata, scrubs): """ for rgx_find, rgx_replace in scrubs: - strdata = re.sub(rgx_find, re.escape(rgx_replace), strdata) + strdata = re.sub(rgx_find, rgx_replace.replace("\\", "\\\\"), strdata) return strdata -def main(): # pragma: not covered - """Command-line access to test_farm. +def main(): # pragma: debugging + """Command-line access to farm tests. Commands: @@ -392,21 +365,19 @@ def main(): # pragma: not covered if op == 'run': # Run the test for real. - for test_case in sys.argv[2:]: - case = FarmTestCase(test_case) - case.run_fully() + for filename in sys.argv[2:]: + FarmTestCase(filename).run_fully() elif op == 'out': # Run the test, but don't clean up, so we can examine the output. - for test_case in sys.argv[2:]: - case = FarmTestCase(test_case, dont_clean=True) - case.run_fully() + for filename in sys.argv[2:]: + FarmTestCase(filename, dont_clean=True).run_fully() elif op == 'clean': # Run all the tests, but just clean. - for test in test_farm(clean_only=True): - test[0].run_fully() + for filename in TEST_FILES: + FarmTestCase(filename, clean_only=True).run_fully() else: print(main.__doc__) # So that we can run just one farm run.py at a time. -if __name__ == '__main__': +if __name__ == '__main__': # pragma: debugging main() diff --git a/tests/test_files.py b/tests/test_files.py index 2d22730..dadb22b 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -38,7 +38,7 @@ class FilesTest(CoverageTest): a1 = self.abs_path("sub/proj1/file1.py") a2 = self.abs_path("sub/proj2/file2.py") d = os.path.normpath("sub/proj1") - os.chdir(d) + self.chdir(d) files.set_relative_directory() self.assertEqual(files.relative_filename(a1), "file1.py") self.assertEqual(files.relative_filename(a2), a2) @@ -163,18 +163,18 @@ class PathAliasesTest(CoverageTest): """ self.assertEqual(aliases.map(inp), files.canonical_filename(out)) - def assert_not_mapped(self, aliases, inp): + def assert_unchanged(self, aliases, inp): """Assert that `inp` mapped through `aliases` is unchanged.""" self.assertEqual(aliases.map(inp), inp) def test_noop(self): aliases = PathAliases() - self.assert_not_mapped(aliases, '/ned/home/a.py') + self.assert_unchanged(aliases, '/ned/home/a.py') def test_nomatch(self): aliases = PathAliases() aliases.add('/home/*/src', './mysrc') - self.assert_not_mapped(aliases, '/home/foo/a.py') + self.assert_unchanged(aliases, '/home/foo/a.py') def test_wildcard(self): aliases = PathAliases() @@ -188,7 +188,7 @@ class PathAliasesTest(CoverageTest): def test_no_accidental_match(self): aliases = PathAliases() aliases.add('/home/*/src', './mysrc') - self.assert_not_mapped(aliases, '/home/foo/srcetc') + self.assert_unchanged(aliases, '/home/foo/srcetc') def test_multiple_patterns(self): aliases = PathAliases() @@ -284,4 +284,4 @@ class WindowsFileTest(CoverageTest): super(WindowsFileTest, self).setUp() def test_actual_path(self): - self.assertEquals(actual_path(r'c:\Windows'), actual_path(r'C:\wINDOWS')) + self.assertEqual(actual_path(r'c:\Windows'), actual_path(r'C:\wINDOWS')) diff --git a/tests/test_html.py b/tests/test_html.py index 1df602f..9bb8f39 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -6,6 +6,7 @@ import datetime import glob +import json import os import os.path import re @@ -46,7 +47,7 @@ class HtmlTestHelpers(CoverageTest): self.clean_local_file_imports() cov = coverage.Coverage(**(covargs or {})) self.start_import_stop(cov, "main_file") - cov.html_report(**(htmlargs or {})) + return cov.html_report(**(htmlargs or {})) def remove_html_files(self): """Remove the HTML files created as part of the HTML report.""" @@ -101,11 +102,7 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): # At least one of our tests monkey-patches the version of coverage.py, # so grab it here to restore it later. self.real_coverage_version = coverage.__version__ - self.addCleanup(self.cleanup_coverage_version) - - def cleanup_coverage_version(self): - """A cleanup.""" - coverage.__version__ = self.real_coverage_version + self.addCleanup(setattr, coverage, "__version__", self.real_coverage_version) def test_html_created(self): # Test basic HTML generation: files should be created. @@ -208,6 +205,44 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) self.assertMultiLineEqual(index1, fixed_index2) + def test_file_becomes_100(self): + self.create_initial_files() + self.run_coverage() + + # Now change a file and do it again + self.make_file("main_file.py", """\ + import helper1, helper2 + # helper1 is now 100% + helper1.func1(12) + helper1.func1(23) + """) + + self.run_coverage(htmlargs=dict(skip_covered=True)) + + # The 100% file, skipped, shouldn't be here. + self.assert_doesnt_exist("htmlcov/helper1_py.html") + + def test_status_format_change(self): + self.create_initial_files() + self.run_coverage() + self.remove_html_files() + + with open("htmlcov/status.json") as status_json: + status_data = json.load(status_json) + + self.assertEqual(status_data['format'], 1) + status_data['format'] = 2 + with open("htmlcov/status.json", "w") as status_json: + json.dump(status_data, status_json) + + self.run_coverage() + + # All the files have been reported again. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1_py.html") + self.assert_exists("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/helper2_py.html") + class HtmlTitleTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML title support.""" @@ -260,18 +295,20 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): """Test the behavior when measuring unparsable files.""" def test_dotpy_not_python(self): + self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 1") cov = coverage.Coverage() - self.start_import_stop(cov, "innocuous") + self.start_import_stop(cov, "main") self.make_file("innocuous.py", "<h1>This isn't python!</h1>") msg = "Couldn't parse '.*innocuous.py' as Python source: .* at line 1" with self.assertRaisesRegex(NotPython, msg): cov.html_report() def test_dotpy_not_python_ignored(self): + self.make_file("main.py", "import innocuous") self.make_file("innocuous.py", "a = 2") cov = coverage.Coverage() - self.start_import_stop(cov, "innocuous") + self.start_import_stop(cov, "main") self.make_file("innocuous.py", "<h1>This isn't python!</h1>") cov.html_report(ignore_errors=True) self.assertEqual( @@ -337,7 +374,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): self.make_file("main.py", "import sub.not_ascii") self.make_file("sub/__init__.py") self.make_file("sub/not_ascii.py", """\ - # coding: utf8 + # coding: utf-8 a = 1 # Isn't this great?! """) cov = coverage.Coverage() @@ -346,7 +383,7 @@ class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): # Create the undecodable version of the file. make_file is too helpful, # so get down and dirty with bytes. with open("sub/not_ascii.py", "wb") as f: - f.write(b"# coding: utf8\na = 1 # Isn't this great?\xcb!\n") + f.write(b"# coding: utf-8\na = 1 # Isn't this great?\xcb!\n") with open("sub/not_ascii.py", "rb") as f: undecodable = f.read() @@ -425,18 +462,58 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): self.run_coverage() self.assert_exists("htmlcov/status.dat") + def test_report_skip_covered_no_branches(self): + self.make_file("main_file.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + self.run_coverage(htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + + def test_report_skip_covered_100(self): + self.make_file("main_file.py", """ + def normal(): + print("z") + normal() + """) + res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) + self.assertEqual(res, 100.0) + self.assert_doesnt_exist("htmlcov/main_file_py.html") + + def test_report_skip_covered_branches(self): + self.make_file("main_file.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + class HtmlStaticFileTest(CoverageTest): """Tests of the static file copying for the HTML report.""" def setUp(self): super(HtmlStaticFileTest, self).setUp() - self.original_path = list(coverage.html.STATIC_PATH) - self.addCleanup(self.cleanup_static_path) - - def cleanup_static_path(self): - """A cleanup.""" - coverage.html.STATIC_PATH = self.original_path + original_path = list(coverage.html.STATIC_PATH) + self.addCleanup(setattr, coverage.html, 'STATIC_PATH', original_path) def test_copying_static_files_from_system(self): # Make a new place for static files. @@ -686,7 +763,7 @@ class HtmlGoldTests(CoverageGoldTest): with change_dir("src"): # pylint: disable=import-error - cov = coverage.Coverage(branch=True) + cov = coverage.Coverage(config_file="partial.ini") cov.start() import partial # pragma: nested cov.stop() # pragma: nested @@ -700,6 +777,8 @@ class HtmlGoldTests(CoverageGoldTest): '<p id="t14" class="stm run hide_run">', # The "if 0" and "if 1" statements are optimized away. '<p id="t17" class="pln">', + # The "raise AssertionError" is excluded by regex in the .ini. + '<p id="t24" class="exc">', ) contains( "out/partial/index.html", diff --git a/tests/test_misc.py b/tests/test_misc.py index 38be345..939b1c9 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -3,11 +3,10 @@ """Tests of miscellaneous stuff.""" -import sys +import pytest -import coverage -from coverage.version import _make_url, _make_version -from coverage.misc import Hasher, file_be_gone +from coverage.misc import contract, dummy_decorator_with_args, file_be_gone +from coverage.misc import format_lines, Hasher, one_of from tests.coveragetest import CoverageTest @@ -34,6 +33,13 @@ class HasherTest(CoverageTest): h2.update(b"Goodbye!") self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + def test_unicode_hashing(self): + h1 = Hasher() + h1.update(u"Hello, world! \N{SNOWMAN}") + h2 = Hasher() + h2.update(u"Goodbye!") + self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + def test_dict_hashing(self): h1 = Hasher() h1.update({'a': 17, 'b': 23}) @@ -62,63 +68,63 @@ class RemoveFileTest(CoverageTest): file_be_gone(".") -class VersionTest(CoverageTest): - """Tests of version.py""" - - run_in_temp_dir = False - - def test_version_info(self): - # Make sure we didn't screw up the version_info tuple. - self.assertIsInstance(coverage.version_info, tuple) - self.assertEqual([type(d) for d in coverage.version_info], [int, int, int, str, int]) - self.assertIn(coverage.version_info[3], ['alpha', 'beta', 'candidate', 'final']) - - def test_make_version(self): - self.assertEqual(_make_version(4, 0, 0, 'alpha', 0), "4.0a0") - self.assertEqual(_make_version(4, 0, 0, 'alpha', 1), "4.0a1") - self.assertEqual(_make_version(4, 0, 0, 'final', 0), "4.0") - self.assertEqual(_make_version(4, 1, 2, 'beta', 3), "4.1.2b3") - self.assertEqual(_make_version(4, 1, 2, 'final', 0), "4.1.2") - self.assertEqual(_make_version(5, 10, 2, 'candidate', 7), "5.10.2rc7") - - def test_make_url(self): - self.assertEqual( - _make_url(4, 0, 0, 'final', 0), - "https://coverage.readthedocs.io" - ) - self.assertEqual( - _make_url(4, 1, 2, 'beta', 3), - "https://coverage.readthedocs.io/en/coverage-4.1.2b3" - ) - - -class SetupPyTest(CoverageTest): - """Tests of setup.py""" +class ContractTest(CoverageTest): + """Tests of our contract decorators.""" run_in_temp_dir = False - def test_metadata(self): - status, output = self.run_command_status( - "python setup.py --description --version --url --author" - ) - self.assertEqual(status, 0) - out = output.splitlines() - self.assertIn("measurement", out[0]) - self.assertEqual(out[1], coverage.__version__) - self.assertEqual(out[2], coverage.__url__) - self.assertIn("Ned Batchelder", out[3]) - - def test_more_metadata(self): - # Let's be sure we pick up our own setup.py - # CoverageTest restores the original sys.path for us. - sys.path.insert(0, '') - from setup import setup_args - - classifiers = setup_args['classifiers'] - self.assertGreater(len(classifiers), 7) - self.assert_starts_with(classifiers[-1], "Development Status ::") - - long_description = setup_args['long_description'].splitlines() - self.assertGreater(len(long_description), 7) - self.assertNotEqual(long_description[0].strip(), "") - self.assertNotEqual(long_description[-1].strip(), "") + def test_bytes(self): + @contract(text='bytes|None') + def need_bytes(text=None): # pylint: disable=missing-docstring + return text + + assert need_bytes(b"Hey") == b"Hey" + assert need_bytes() is None + with pytest.raises(Exception): + need_bytes(u"Oops") + + def test_unicode(self): + @contract(text='unicode|None') + def need_unicode(text=None): # pylint: disable=missing-docstring + return text + + assert need_unicode(u"Hey") == u"Hey" + assert need_unicode() is None + with pytest.raises(Exception): + need_unicode(b"Oops") + + def test_one_of(self): + @one_of("a, b, c") + def give_me_one(a=None, b=None, c=None): # pylint: disable=missing-docstring + return (a, b, c) + + assert give_me_one(a=17) == (17, None, None) + assert give_me_one(b=set()) == (None, set(), None) + assert give_me_one(c=17) == (None, None, 17) + with pytest.raises(AssertionError): + give_me_one(a=17, b=set()) + with pytest.raises(AssertionError): + give_me_one() + + def test_dummy_decorator_with_args(self): + @dummy_decorator_with_args("anything", this=17, that="is fine") + def undecorated(a=None, b=None): # pylint: disable=missing-docstring + return (a, b) + + assert undecorated() == (None, None) + assert undecorated(17) == (17, None) + assert undecorated(b=23) == (None, 23) + assert undecorated(b=42, a=3) == (3, 42) + + +@pytest.mark.parametrize("statements, lines, result", [ + (set([1,2,3,4,5,10,11,12,13,14]), set([1,2,5,10,11,13,14]), "1-2, 5-11, 13-14"), + ([1,2,3,4,5,10,11,12,13,14,98,99], [1,2,5,10,11,13,14,99], "1-2, 5-11, 13-14, 99"), + ([1,2,3,4,98,99,100,101,102,103,104], [1,2,99,102,103,104], "1-2, 99, 102-104"), + ([17], [17], "17"), + ([90,91,92,93,94,95], [90,91,92,93,94,95], "90-95"), + ([1, 2, 3, 4, 5], [], ""), + ([1, 2, 3, 4, 5], [4], "4"), +]) +def test_format_lines(statements, lines, result): + assert format_lines(statements, lines) == result diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 87c65b0..b54f4ef 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -5,7 +5,12 @@ import sys +from flaky import flaky +import pytest + import coverage +from coverage import env +from coverage.backward import import_local_file from coverage.files import abs_file from tests.coveragetest import CoverageTest @@ -115,7 +120,7 @@ class RecursionTest(CoverageTest): pytrace = (cov.collector.tracer_name() == "PyTracer") expected_missing = [3] - if pytrace: + if pytrace: # pragma: no metacov expected_missing += [9, 10, 11] _, statements, missing, _ = cov.analysis("recur.py") @@ -123,7 +128,7 @@ class RecursionTest(CoverageTest): self.assertEqual(missing, expected_missing) # Get a warning about the stackoverflow effect on the tracing function. - if pytrace: + if pytrace: # pragma: no metacov self.assertEqual(cov._warnings, ["Trace function changed, measurement is likely wrong: None"] ) @@ -140,7 +145,11 @@ class MemoryLeakTest(CoverageTest): It may still fail occasionally, especially on PyPy. """ + @flaky def test_for_leaks(self): + if env.JYTHON: + self.skipTest("Don't bother on Jython") + # Our original bad memory leak only happened on line numbers > 255, so # make a code object with more lines than that. Ugly string mumbo # jumbo to get 300 blank lines at the beginning.. @@ -176,17 +185,56 @@ class MemoryLeakTest(CoverageTest): # Running the code 10k times shouldn't grow the ram much more than # running it 10 times. ram_growth = (ram_10k - ram_10) - (ram_10 - ram_0) - if ram_growth > 100000: # pragma: only failure - fails += 1 - - if fails > 8: # pragma: only failure - self.fail("RAM grew by %d" % (ram_growth)) + if ram_growth > 100000: + fails += 1 # pragma: only failure + + if fails > 8: + self.fail("RAM grew by %d" % (ram_growth)) # pragma: only failure + + +class MemoryFumblingTest(CoverageTest): + """Test that we properly manage the None refcount.""" + + def test_dropping_none(self): # pragma: not covered + if not env.C_TRACER: + self.skipTest("Only the C tracer has refcounting issues") + # TODO: Mark this so it will only be run sometimes. + self.skipTest("This is too expensive for now (30s)") + # Start and stop coverage thousands of times to flush out bad + # reference counting, maybe. + self.make_file("the_code.py", """\ + import random + def f(): + if random.random() > .5: + x = 1 + else: + x = 2 + """) + self.make_file("main.py", """\ + import coverage + import sys + from the_code import f + for i in range(10000): + cov = coverage.Coverage(branch=True) + cov.start() + f() + cov.stop() + cov.erase() + print("Final None refcount: %d" % (sys.getrefcount(None))) + """) + status, out = self.run_command_status("python main.py") + self.assertEqual(status, 0) + self.assertIn("Final None refcount", out) + self.assertNotIn("Fatal", out) class PyexpatTest(CoverageTest): """Pyexpat screws up tracing. Make sure we've counter-defended properly.""" def test_pyexpat(self): + if env.JYTHON: + self.skipTest("Pyexpat isn't a problem on Jython") + # pyexpat calls the trace function explicitly (inexplicably), and does # it wrong for exceptions. Parsing a DOCTYPE for some reason throws # an exception internally, and triggers its wrong behavior. This test @@ -286,7 +334,7 @@ class ExceptionTest(CoverageTest): # Import all the modules before starting coverage, so the def lines # won't be in all the results. for mod in "oops fly catch doit".split(): - self.import_local_file(mod) + import_local_file(mod) # Each run nests the functions differently to get different # combinations of catching exceptions and letting them fly. @@ -337,6 +385,13 @@ class ExceptionTest(CoverageTest): lines = data.lines(abs_file(filename)) clean_lines[filename] = sorted(lines) + if env.JYTHON: # pragma: only jython + # Jython doesn't report on try or except lines, so take those + # out of the expected lines. + invisible = [202, 206, 302, 304] + for lines in lines_expected.values(): + lines[:] = [l for l in lines if l not in invisible] + self.assertEqual(clean_lines, lines_expected) @@ -346,14 +401,11 @@ class DoctestTest(CoverageTest): def setUp(self): super(DoctestTest, self).setUp() - # Oh, the irony! This test case exists because Python 2.4's - # doctest module doesn't play well with coverage. But nose fixes - # the problem by monkeypatching doctest. I want to undo the - # monkeypatch to be sure I'm getting the doctest module that users - # of coverage will get. Deleting the imported module here is - # enough: when the test imports doctest again, it will get a fresh - # copy without the monkeypatch. - del sys.modules['doctest'] + # This test case exists because Python 2.4's doctest module didn't play + # well with coverage. Nose fixes the problem by monkeypatching doctest. + # I want to be sure there's no monkeypatch and that I'm getting the + # doctest module that users of coverage will get. + assert 'doctest' not in sys.modules def test_doctest(self): self.check_coverage('''\ @@ -380,35 +432,38 @@ class DoctestTest(CoverageTest): class GettraceTest(CoverageTest): """Tests that we work properly with `sys.gettrace()`.""" - def test_round_trip(self): - self.check_coverage('''\ - import sys - def foo(n): - return 3*n - def bar(n): - return 5*n - a = foo(6) - sys.settrace(sys.gettrace()) - a = bar(8) - ''', - [1, 2, 3, 4, 5, 6, 7, 8], "") - - def test_multi_layers(self): - self.check_coverage('''\ + def test_round_trip_in_untraced_function(self): + # https://bitbucket.org/ned/coveragepy/issues/575/running-doctest-prevents-complete-coverage + self.make_file("main.py", """import sample""") + self.make_file("sample.py", """\ + from swap import swap_it + def doit(): + print(3) + swap_it() + print(5) + def doit_soon(): + print(7) + doit() + print(9) + print(10) + doit_soon() + print(12) + """) + self.make_file("swap.py", """\ import sys - def level1(): - a = 3 - level2() - b = 5 - def level2(): - c = 7 + def swap_it(): sys.settrace(sys.gettrace()) - d = 9 - e = 10 - level1() - f = 12 - ''', - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "") + """) + + # Use --source=sample to prevent measurement of swap.py. + cov = coverage.Coverage(source=["sample"]) + self.start_import_stop(cov, "main") + + self.assertEqual(self.stdout(), "10\n7\n3\n5\n9\n12\n") + + _, statements, missing, _ = cov.analysis("sample.py") + self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) + self.assertEqual(missing, []) def test_setting_new_trace_function(self): # https://bitbucket.org/ned/coveragepy/issues/436/disabled-coverage-ctracer-may-rise-from @@ -433,8 +488,10 @@ class GettraceTest(CoverageTest): old = sys.gettrace() test_unsets_trace() sys.settrace(old) + a = 21 + b = 22 ''', - lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20], + lines=[1, 3, 4, 5, 7, 8, 10, 11, 12, 14, 15, 16, 18, 19, 20, 21, 22], missing="4-5, 11-12", ) @@ -450,6 +507,38 @@ class GettraceTest(CoverageTest): ), ) + @pytest.mark.expensive + def test_atexit_gettrace(self): # pragma: no metacov + # This is not a test of coverage at all, but of our understanding + # of this edge-case behavior in various Pythons. + if env.METACOV: + self.skipTest("Can't set trace functions during meta-coverage") + + self.make_file("atexit_gettrace.py", """\ + import atexit, sys + + def trace_function(frame, event, arg): + return trace_function + sys.settrace(trace_function) + + def show_trace_function(): + tfn = sys.gettrace() + if tfn is not None: + tfn = tfn.__name__ + print(tfn) + atexit.register(show_trace_function) + + # This will show what the trace function is at the end of the program. + """) + status, out = self.run_command_status("python atexit_gettrace.py") + self.assertEqual(status, 0) + if env.PYPY and env.PYPYVERSION >= (5, 4): + # Newer PyPy clears the trace function before atexit runs. + self.assertEqual(out, "None\n") + else: + # Other Pythons leave the trace function in place. + self.assertEqual(out, "trace_function\n") + class ExecTest(CoverageTest): """Tests of exec.""" diff --git a/tests/test_parser.py b/tests/test_parser.py index 5fee823..aa96b59 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -193,7 +193,7 @@ class ParserMissingArcDescriptionTest(CoverageTest): def parse_text(self, source): """Parse Python source, and return the parser object.""" - parser = PythonParser(textwrap.dedent(source)) + parser = PythonParser(text=textwrap.dedent(source)) parser.parse_source() return parser diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index ccbe01b..ddb652e 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -5,6 +5,7 @@ import os.path import re +import textwrap from coverage import env from coverage.phystokens import source_token_lines, source_encoding @@ -14,18 +15,35 @@ from coverage.python import get_python_source from tests.coveragetest import CoverageTest +# A simple program and its token stream. SIMPLE = u"""\ # yay! def foo(): say('two = %d' % 2) """ +SIMPLE_TOKENS = [ + [('com', "# yay!")], + [('key', 'def'), ('ws', ' '), ('nam', 'foo'), ('op', '('), ('op', ')'), ('op', ':')], + [('ws', ' '), ('nam', 'say'), ('op', '('), + ('str', "'two = %d'"), ('ws', ' '), ('op', '%'), + ('ws', ' '), ('num', '2'), ('op', ')')], +] + +# Mixed-whitespace program, and its token stream. MIXED_WS = u"""\ def hello(): a="Hello world!" \tb="indented" """ +MIXED_WS_TOKENS = [ + [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ('op', ')'), ('op', ':')], + [('ws', ' '), ('nam', 'a'), ('op', '='), ('str', '"Hello world!"')], + [('ws', ' '), ('nam', 'b'), ('op', '='), ('str', '"indented"')], +] + +# Where this file is, so we can find other files next to it. HERE = os.path.dirname(__file__) @@ -52,28 +70,16 @@ class PhysTokensTest(CoverageTest): self.check_tokenization(get_python_source(fname)) def test_simple(self): - self.assertEqual(list(source_token_lines(SIMPLE)), - [ - [('com', "# yay!")], - [('key', 'def'), ('ws', ' '), ('nam', 'foo'), ('op', '('), - ('op', ')'), ('op', ':')], - [('ws', ' '), ('nam', 'say'), ('op', '('), - ('str', "'two = %d'"), ('ws', ' '), ('op', '%'), - ('ws', ' '), ('num', '2'), ('op', ')')] - ]) + self.assertEqual(list(source_token_lines(SIMPLE)), SIMPLE_TOKENS) self.check_tokenization(SIMPLE) + def test_missing_final_newline(self): + # We can tokenize source that is missing the final newline. + self.assertEqual(list(source_token_lines(SIMPLE.rstrip())), SIMPLE_TOKENS) + def test_tab_indentation(self): # Mixed tabs and spaces... - self.assertEqual(list(source_token_lines(MIXED_WS)), - [ - [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), - ('op', ')'), ('op', ':')], - [('ws', ' '), ('nam', 'a'), ('op', '='), - ('str', '"Hello world!"')], - [('ws', ' '), ('nam', 'b'), ('op', '='), - ('str', '"indented"')], - ]) + self.assertEqual(list(source_token_lines(MIXED_WS)), MIXED_WS_TOKENS) def test_tokenize_real_file(self): # Check the tokenization of a real file (large, btw). @@ -97,13 +103,15 @@ else: ENCODING_DECLARATION_SOURCES = [ # Various forms from http://www.python.org/dev/peps/pep-0263/ - (1, b"# coding=cp850\n\n"), - (1, b"#!/usr/bin/python\n# -*- coding: cp850 -*-\n"), - (1, b"#!/usr/bin/python\n# vim: set fileencoding=cp850:\n"), - (1, b"# This Python file uses this encoding: cp850\n"), - (1, b"# This file uses a different encoding:\n# coding: cp850\n"), - (1, b"\n# coding=cp850\n\n"), - (2, b"# -*- coding:cp850 -*-\n# vim: fileencoding=cp850\n"), + (1, b"# coding=cp850\n\n", "cp850"), + (1, b"# coding=latin-1\n", "iso-8859-1"), + (1, b"# coding=iso-latin-1\n", "iso-8859-1"), + (1, b"#!/usr/bin/python\n# -*- coding: cp850 -*-\n", "cp850"), + (1, b"#!/usr/bin/python\n# vim: set fileencoding=cp850:\n", "cp850"), + (1, b"# This Python file uses this encoding: cp850\n", "cp850"), + (1, b"# This file uses a different encoding:\n# coding: cp850\n", "cp850"), + (1, b"\n# coding=cp850\n\n", "cp850"), + (2, b"# -*- coding:cp850 -*-\n# vim: fileencoding=cp850\n", "cp850"), ] class SourceEncodingTest(CoverageTest): @@ -112,15 +120,15 @@ class SourceEncodingTest(CoverageTest): run_in_temp_dir = False def test_detect_source_encoding(self): - for _, source in ENCODING_DECLARATION_SOURCES: + for _, source, expected in ENCODING_DECLARATION_SOURCES: self.assertEqual( source_encoding(source), - 'cp850', + expected, "Wrong encoding in %r" % source ) def test_detect_source_encoding_not_in_comment(self): - if env.PYPY and env.PY3: + if env.PYPY and env.PY3: # pragma: no metacov # PyPy3 gets this case wrong. Not sure what I can do about it, # so skip the test. self.skipTest("PyPy3 is wrong about non-comment encoding. Skip it.") @@ -142,9 +150,19 @@ class SourceEncodingTest(CoverageTest): source = b"\xEF\xBB\xBFtext = 'hello'\n" self.assertEqual(source_encoding(source), 'utf-8-sig') - # But it has to be the only authority. + def test_bom_with_encoding(self): + source = b"\xEF\xBB\xBF# coding: utf-8\ntext = 'hello'\n" + self.assertEqual(source_encoding(source), 'utf-8-sig') + + def test_bom_is_wrong(self): + # A BOM with an explicit non-utf8 encoding is an error. source = b"\xEF\xBB\xBF# coding: cp850\n" - with self.assertRaises(SyntaxError): + with self.assertRaisesRegex(SyntaxError, "encoding problem: utf-8"): + source_encoding(source) + + def test_unknown_encoding(self): + source = b"# coding: klingon\n" + with self.assertRaisesRegex(SyntaxError, "unknown encoding: klingon"): source_encoding(source) @@ -154,7 +172,7 @@ class NeuterEncodingDeclarationTest(CoverageTest): run_in_temp_dir = False def test_neuter_encoding_declaration(self): - for lines_diff_expected, source in ENCODING_DECLARATION_SOURCES: + for lines_diff_expected, source, _ in ENCODING_DECLARATION_SOURCES: neutered = neuter_encoding_declaration(source.decode("ascii")) neutered = neutered.encode("ascii") @@ -177,6 +195,67 @@ class NeuterEncodingDeclarationTest(CoverageTest): "Wrong encoding in %r" % neutered ) + def test_two_encoding_declarations(self): + input_src = textwrap.dedent(u"""\ + # -*- coding: ascii -*- + # -*- coding: utf-8 -*- + # -*- coding: utf-16 -*- + """) + expected_src = textwrap.dedent(u"""\ + # (deleted declaration) -*- + # (deleted declaration) -*- + # -*- coding: utf-16 -*- + """) + output_src = neuter_encoding_declaration(input_src) + self.assertEqual(expected_src, output_src) + + def test_one_encoding_declaration(self): + input_src = textwrap.dedent(u"""\ + # -*- coding: utf-16 -*- + # Just a comment. + # -*- coding: ascii -*- + """) + expected_src = textwrap.dedent(u"""\ + # (deleted declaration) -*- + # Just a comment. + # -*- coding: ascii -*- + """) + output_src = neuter_encoding_declaration(input_src) + self.assertEqual(expected_src, output_src) + + +class Bug529Test(CoverageTest): + """Test of bug 529""" + + def test_bug_529(self): + # Don't over-neuter coding declarations. This happened with a test + # file which contained code in multi-line strings, all with coding + # declarations. The neutering of the file also changed the multi-line + # strings, which it shouldn't have. + self.make_file("the_test.py", '''\ + # -*- coding: utf-8 -*- + import unittest + class Bug529Test(unittest.TestCase): + def test_two_strings_are_equal(self): + src1 = u"""\\ + # -*- coding: utf-8 -*- + # Just a comment. + """ + src2 = u"""\\ + # -*- coding: utf-8 -*- + # Just a comment. + """ + self.assertEqual(src1, src2) + + if __name__ == "__main__": + unittest.main() + ''') + status, out = self.run_command_status("coverage run the_test.py") + self.assertEqual(status, 0) + self.assertIn("OK", out) + # If this test fails, the output will be super-confusing, because it + # has a failing unit test contained within the failing unit test. + class CompileUnicodeTest(CoverageTest): """Tests of compiling Unicode strings.""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 8ea0a8f..5486216 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -333,9 +333,8 @@ class GoodPluginTest(FileTracerTest): # quux_5.html will be omitted from the results. assert render("quux_5.html", 3) == "[quux_5.html @ 3]" - # In Python 2, either kind of string should be OK. - if sys.version_info[0] == 2: - assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]" + # For Python 2, make sure unicode is working. + assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]" """) # will try to read the actual source files, so make some @@ -372,11 +371,10 @@ class GoodPluginTest(FileTracerTest): self.assertNotIn("quux_5.html", cov.data.line_counts()) - if env.PY2: - _, statements, missing, _ = cov.analysis("uni_3.html") - self.assertEqual(statements, [1, 2, 3]) - self.assertEqual(missing, [1]) - self.assertIn("uni_3.html", cov.data.line_counts()) + _, statements, missing, _ = cov.analysis("uni_3.html") + self.assertEqual(statements, [1, 2, 3]) + self.assertEqual(missing, [1]) + self.assertIn("uni_3.html", cov.data.line_counts()) def test_plugin2_with_branch(self): self.make_render_and_caller() @@ -507,6 +505,58 @@ class GoodPluginTest(FileTracerTest): self.assertEqual(report, expected) self.assertEqual(total, 50) + def test_find_unexecuted(self): + self.make_file("unexecuted_plugin.py", """\ + import os + import coverage.plugin + class Plugin(coverage.CoveragePlugin): + def file_tracer(self, filename): + if filename.endswith("foo.py"): + return MyTracer(filename) + def file_reporter(self, filename): + return MyReporter(filename) + def find_executable_files(self, src_dir): + # Check that src_dir is the right value + files = os.listdir(src_dir) + assert "foo.py" in files + assert "unexecuted_plugin.py" in files + return ["chimera.py"] + + class MyTracer(coverage.plugin.FileTracer): + def __init__(self, filename): + self.filename = filename + def source_filename(self): + return self.filename + def line_number_range(self, frame): + return (999, 999) + + class MyReporter(coverage.FileReporter): + def lines(self): + return set([99, 999, 9999]) + + def coverage_init(reg, options): + reg.add_file_tracer(Plugin()) + """) + self.make_file("foo.py", "a = 1\n") + cov = coverage.Coverage(source=['.']) + cov.set_option("run:plugins", ["unexecuted_plugin"]) + self.start_import_stop(cov, "foo") + + # The file we executed claims to have run line 999. + _, statements, missing, _ = cov.analysis("foo.py") + self.assertEqual(statements, [99, 999, 9999]) + self.assertEqual(missing, [99, 9999]) + + # The completely missing file is in the results. + _, statements, missing, _ = cov.analysis("chimera.py") + self.assertEqual(statements, [99, 999, 9999]) + self.assertEqual(missing, [99, 999, 9999]) + + # But completely new filenames are not in the results. + self.assertEqual(len(cov.get_data().measured_files()), 3) + with self.assertRaises(CoverageException): + cov.analysis("fictional.py") + class BadPluginTest(FileTracerTest): """Test error handling around plugins.""" @@ -542,7 +592,7 @@ class BadPluginTest(FileTracerTest): self.start_import_stop(cov, "simple") return cov - def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None): + def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, excmsgs=None): """Run a file, and see that the plugin failed. `module_name` and `plugin_name` is the module and name of the plugin to @@ -551,7 +601,10 @@ class BadPluginTest(FileTracerTest): `our_error` is True if the error reported to the user will be an explicit error in our test code, marked with an '# Oh noes!' comment. - `excmsg`, if provided, is text that should appear in the stderr. + `excmsg`, if provided, is text that must appear in the stderr. + + `excmsgs`, if provided, is a list of messages, one of which must + appear in the stderr. The plugin will be disabled, and we check that a warning is output explaining why. @@ -560,7 +613,6 @@ class BadPluginTest(FileTracerTest): self.run_plugin(module_name) stderr = self.stderr() - print(stderr) # for diagnosing test failures. if our_error: errors = stderr.count("# Oh noes!") @@ -578,6 +630,8 @@ class BadPluginTest(FileTracerTest): if excmsg: self.assertIn(excmsg, stderr) + if excmsgs: + self.assertTrue(any(em in stderr for em in excmsgs), "expected one of %r" % excmsgs) def test_file_tracer_has_no_file_tracer_method(self): self.make_file("bad_plugin.py", """\ @@ -650,7 +704,9 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="'float' object has no attribute", + ) def test_has_dynamic_source_filename_fails(self): self.make_file("bad_plugin.py", """\ @@ -698,7 +754,15 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, + excmsgs=[ + "expected str, bytes or os.PathLike object, not float", + "'float' object has no attribute", + "object of type 'float' has no len()", + "'float' object is unsubscriptable", + ], + ) def test_dynamic_source_filename_fails(self): self.make_file("bad_plugin.py", """\ @@ -737,7 +801,9 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", + ) def test_line_number_range_returns_triple(self): self.make_file("bad_plugin.py", """\ @@ -757,7 +823,9 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", + ) def test_line_number_range_returns_pair_of_strings(self): self.make_file("bad_plugin.py", """\ @@ -777,4 +845,6 @@ class BadPluginTest(FileTracerTest): def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) - self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) + self.run_bad_plugin( + "bad_plugin", "Plugin", our_error=False, excmsg="an integer is required", + ) diff --git a/tests/test_process.py b/tests/test_process.py index aa2045f..8a0f4e3 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -1,9 +1,10 @@ -# coding: utf8 +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt """Tests for process behavior of coverage.py.""" +import distutils.sysconfig # pylint: disable=import-error import glob import os import os.path @@ -11,13 +12,14 @@ import re import sys import textwrap +import pytest + import coverage from coverage import env, CoverageData from coverage.misc import output_encoding from tests.coveragetest import CoverageTest - -TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py") +from tests.helpers import re_lines class ProcessTest(CoverageTest): @@ -388,8 +390,7 @@ class ProcessTest(CoverageTest): out2 = self.run_command("python throw.py") if env.PYPY: # Pypy has an extra frame in the traceback for some reason - lines2 = out2.splitlines() - out2 = "".join(l+"\n" for l in lines2 if "toplevel" not in l) + out2 = re_lines(out2, "toplevel", match=False) self.assertMultiLineEqual(out, out2) # But also make sure that the output is what we expect. @@ -436,118 +437,6 @@ class ProcessTest(CoverageTest): self.assertEqual(status, status2) self.assertEqual(status, 0) - def assert_execfile_output(self, out): - """Assert that the output we got is a successful run of try_execfile.py""" - self.assertIn('"DATA": "xyzzy"', out) - - def test_coverage_run_is_like_python(self): - with open(TRY_EXECFILE) as f: - self.make_file("run_me.py", f.read()) - out_cov = self.run_command("coverage run run_me.py") - out_py = self.run_command("python run_me.py") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_is_like_python_dashm(self): - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command("coverage run -m process_test.try_execfile") - out_py = self.run_command("python -m process_test.try_execfile") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dir_is_like_python_dir(self): - with open(TRY_EXECFILE) as f: - self.make_file("with_main/__main__.py", f.read()) - out_cov = self.run_command("coverage run with_main") - out_py = self.run_command("python with_main") - - # The coverage.py results are not identical to the Python results, and - # I don't know why. For now, ignore those failures. If someone finds - # a real problem with the discrepancies, we can work on it some more. - ignored = r"__file__|__loader__|__package__" - # PyPy includes the current directory in the path when running a - # directory, while CPython and coverage.py do not. Exclude that from - # the comparison also... - if env.PYPY: - ignored += "|"+re.escape(os.getcwd()) - out_cov = remove_matching_lines(out_cov, ignored) - out_py = remove_matching_lines(out_py, ignored) - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_equal_to_doubledashsource(self): - """regression test for #328 - - When imported by -m, a module's __name__ is __main__, but we need the - --source machinery to know and respect the original name. - """ - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command( - "coverage run --source process_test.try_execfile -m process_test.try_execfile" - ) - out_py = self.run_command("python -m process_test.try_execfile") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_superset_of_doubledashsource(self): - """Edge case: --source foo -m foo.bar""" - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command( - "coverage run --source process_test -m process_test.try_execfile" - ) - out_py = self.run_command("python -m process_test.try_execfile") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual(self.line_count(out), 6, out) - - def test_coverage_run_script_imports_doubledashsource(self): - # This file imports try_execfile, which compiles it to .pyc, so the - # first run will have __file__ == "try_execfile.py" and the second will - # have __file__ == "try_execfile.pyc", which throws off the comparison. - # Setting dont_write_bytecode True stops the compilation to .pyc and - # keeps the test working. - self.make_file("myscript", """\ - import sys; sys.dont_write_bytecode = True - import process_test.try_execfile - """) - - # These -m commands assume the coverage tree is on the path. - out_cov = self.run_command( - "coverage run --source process_test myscript" - ) - out_py = self.run_command("python myscript") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual(self.line_count(out), 6, out) - - def test_coverage_run_dashm_is_like_python_dashm_off_path(self): - # https://bitbucket.org/ned/coveragepy/issue/242 - self.make_file("sub/__init__.py", "") - with open(TRY_EXECFILE) as f: - self.make_file("sub/run_me.py", f.read()) - out_cov = self.run_command("coverage run -m sub.run_me") - out_py = self.run_command("python -m sub.run_me") - self.assertMultiLineEqual(out_cov, out_py) - self.assert_execfile_output(out_cov) - - def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): - if sys.version_info < (2, 7): - # Coverage.py isn't bug-for-bug compatible in the behavior of -m for - # Pythons < 2.7 - self.skipTest("-m doesn't work the same < Python 2.7") - # https://bitbucket.org/ned/coveragepy/issue/207 - self.make_file("package/__init__.py", "print('init')") - self.make_file("package/__main__.py", "print('main')") - out_cov = self.run_command("coverage run -m package") - out_py = self.run_command("python -m package") - self.assertMultiLineEqual(out_cov, out_py) - def test_fork(self): if not hasattr(os, 'fork'): self.skipTest("Can't test os.fork since it doesn't exist.") @@ -595,21 +484,6 @@ class ProcessTest(CoverageTest): data.read_file(".coverage") self.assertEqual(data.line_counts()['fork.py'], 9) - def test_warnings(self): - self.make_file("hello.py", """\ - import sys, os - print("Hello") - """) - out = self.run_command("coverage run --source=sys,xyzzy,quux hello.py") - - self.assertIn("Hello\n", out) - self.assertIn(textwrap.dedent("""\ - Coverage.py warning: Module sys has no Python source. - Coverage.py warning: Module xyzzy was never imported. - Coverage.py warning: Module quux was never imported. - Coverage.py warning: No data was collected. - """), out) - def test_warnings_during_reporting(self): # While fixing issue #224, the warnings were being printed far too # often. Make sure they're not any more. @@ -692,7 +566,8 @@ class ProcessTest(CoverageTest): self.assertEqual(len(infos), 1) self.assertEqual(infos[0]['note'], u"These are musical notes: ♫𝅗𝅥♩") - def test_fullcoverage(self): # pragma: not covered + @pytest.mark.expensive + def test_fullcoverage(self): # pragma: no metacov if env.PY2: # This doesn't work on Python 2. self.skipTest("fullcoverage doesn't work on Python 2.") # It only works with the C tracer, and if we aren't measuring ourselves. @@ -721,6 +596,26 @@ class ProcessTest(CoverageTest): # about 5. self.assertGreater(data.line_counts()['os.py'], 50) + def test_lang_c(self): + if env.PY3 and sys.version_info < (3, 4): + # Python 3.3 can't compile the non-ascii characters in the file name. + self.skipTest("3.3 can't handle this test") + # LANG=C forces getfilesystemencoding on Linux to 'ascii', which causes + # failures with non-ascii file names. We don't want to make a real file + # with strange characters, though, because that gets the test runners + # tangled up. This will isolate the concerns to the coverage.py code. + # https://bitbucket.org/ned/coveragepy/issues/533/exception-on-unencodable-file-name + self.make_file("weird_file.py", r""" + globs = {} + code = "a = 1\nb = 2\n" + exec(compile(code, "wut\xe9\xea\xeb\xec\x01\x02.py", 'exec'), globs) + print(globs['a']) + print(globs['b']) + """) + self.set_environ("LANG", "C") + out = self.run_command("coverage run weird_file.py") + self.assertEqual(out, "1\n2\n") + def test_deprecation_warnings(self): # Test that coverage doesn't trigger deprecation warnings. # https://bitbucket.org/ned/coveragepy/issue/305/pendingdeprecationwarning-the-imp-module @@ -753,7 +648,8 @@ class ProcessTest(CoverageTest): out = self.run_command("python run_twice.py") self.assertEqual( out, - "Coverage.py warning: Module foo was previously imported, but not measured.\n" + "Coverage.py warning: Module foo was previously imported, but not measured. " + "(module-not-measured)\n" ) def test_module_name(self): @@ -766,11 +662,223 @@ class ProcessTest(CoverageTest): self.assertIn("Use 'coverage help' for help", out) +TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py") + +class EnvironmentTest(CoverageTest): + """Tests using try_execfile.py to test the execution environment.""" + + def assert_tryexecfile_output(self, out1, out2): + """Assert that the output we got is a successful run of try_execfile.py. + + `out1` and `out2` must be the same, modulo a few slight known platform + differences. + + """ + # First, is this even credible try_execfile.py output? + self.assertIn('"DATA": "xyzzy"', out1) + + if env.JYTHON: # pragma: only jython + # Argv0 is different for Jython, remove that from the comparison. + out1 = re_lines(out1, r'\s+"argv0":', match=False) + out2 = re_lines(out2, r'\s+"argv0":', match=False) + + self.assertMultiLineEqual(out1, out2) + + def test_coverage_run_is_like_python(self): + with open(TRY_EXECFILE) as f: + self.make_file("run_me.py", f.read()) + out_cov = self.run_command("coverage run run_me.py") + out_py = self.run_command("python run_me.py") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_is_like_python_dashm(self): + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command("coverage run -m process_test.try_execfile") + out_py = self.run_command("python -m process_test.try_execfile") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dir_is_like_python_dir(self): + with open(TRY_EXECFILE) as f: + self.make_file("with_main/__main__.py", f.read()) + + out_cov = self.run_command("coverage run with_main") + out_py = self.run_command("python with_main") + + # The coverage.py results are not identical to the Python results, and + # I don't know why. For now, ignore those failures. If someone finds + # a real problem with the discrepancies, we can work on it some more. + ignored = r"__file__|__loader__|__package__" + # PyPy includes the current directory in the path when running a + # directory, while CPython and coverage.py do not. Exclude that from + # the comparison also... + if env.PYPY: + ignored += "|"+re.escape(os.getcwd()) + out_cov = re_lines(out_cov, ignored, match=False) + out_py = re_lines(out_py, ignored, match=False) + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_equal_to_doubledashsource(self): + """regression test for #328 + + When imported by -m, a module's __name__ is __main__, but we need the + --source machinery to know and respect the original name. + """ + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source process_test.try_execfile -m process_test.try_execfile" + ) + out_py = self.run_command("python -m process_test.try_execfile") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_superset_of_doubledashsource(self): + """Edge case: --source foo -m foo.bar""" + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source process_test -m process_test.try_execfile" + ) + out_py = self.run_command("python -m process_test.try_execfile") + self.assert_tryexecfile_output(out_cov, out_py) + + st, out = self.run_command_status("coverage report") + self.assertEqual(st, 0) + self.assertEqual(self.line_count(out), 6, out) + + def test_coverage_run_script_imports_doubledashsource(self): + # This file imports try_execfile, which compiles it to .pyc, so the + # first run will have __file__ == "try_execfile.py" and the second will + # have __file__ == "try_execfile.pyc", which throws off the comparison. + # Setting dont_write_bytecode True stops the compilation to .pyc and + # keeps the test working. + self.make_file("myscript", """\ + import sys; sys.dont_write_bytecode = True + import process_test.try_execfile + """) + + # These -m commands assume the coverage tree is on the path. + out_cov = self.run_command( + "coverage run --source process_test myscript" + ) + out_py = self.run_command("python myscript") + self.assert_tryexecfile_output(out_cov, out_py) + + st, out = self.run_command_status("coverage report") + self.assertEqual(st, 0) + self.assertEqual(self.line_count(out), 6, out) + + def test_coverage_run_dashm_is_like_python_dashm_off_path(self): + # https://bitbucket.org/ned/coveragepy/issue/242 + self.make_file("sub/__init__.py", "") + with open(TRY_EXECFILE) as f: + self.make_file("sub/run_me.py", f.read()) + + out_cov = self.run_command("coverage run -m sub.run_me") + out_py = self.run_command("python -m sub.run_me") + self.assert_tryexecfile_output(out_cov, out_py) + + def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): + if sys.version_info < (2, 7): + # Coverage.py isn't bug-for-bug compatible in the behavior + # of -m for Pythons < 2.7 + self.skipTest("-m doesn't work the same < Python 2.7") + # https://bitbucket.org/ned/coveragepy/issue/207 + self.make_file("package/__init__.py", "print('init')") + self.make_file("package/__main__.py", "print('main')") + out_cov = self.run_command("coverage run -m package") + out_py = self.run_command("python -m package") + self.assertMultiLineEqual(out_cov, out_py) + + +class ExcepthookTest(CoverageTest): + """Tests of sys.excepthook support.""" + + def test_excepthook(self): + self.make_file("excepthook.py", """\ + import sys + + def excepthook(*args): + print('in excepthook') + if maybe == 2: + print('definitely') + + sys.excepthook = excepthook + + maybe = 1 + raise RuntimeError('Error Outside') + """) + cov_st, cov_out = self.run_command_status("coverage run excepthook.py") + py_st, py_out = self.run_command_status("python excepthook.py") + if not env.JYTHON: + self.assertEqual(cov_st, py_st) + self.assertEqual(cov_st, 1) + + self.assertIn("in excepthook", py_out) + self.assertEqual(cov_out, py_out) + + # Read the coverage file and see that excepthook.py has 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.line_counts()['excepthook.py'], 7) + + def test_excepthook_exit(self): + if env.PYPY or env.JYTHON: + self.skipTest("non-CPython handles excepthook exits differently, punt for now.") + self.make_file("excepthook_exit.py", """\ + import sys + + def excepthook(*args): + print('in excepthook') + sys.exit(0) + + sys.excepthook = excepthook + + raise RuntimeError('Error Outside') + """) + cov_st, cov_out = self.run_command_status("coverage run excepthook_exit.py") + py_st, py_out = self.run_command_status("python excepthook_exit.py") + self.assertEqual(cov_st, py_st) + self.assertEqual(cov_st, 0) + + self.assertIn("in excepthook", py_out) + self.assertEqual(cov_out, py_out) + + def test_excepthook_throw(self): + if env.PYPY: + self.skipTest("PyPy handles excepthook throws differently, punt for now.") + self.make_file("excepthook_throw.py", """\ + import sys + + def excepthook(*args): + # Write this message to stderr so that we don't have to deal + # with interleaved stdout/stderr comparisons in the assertions + # in the test. + sys.stderr.write('in excepthook\\n') + raise RuntimeError('Error Inside') + + sys.excepthook = excepthook + + raise RuntimeError('Error Outside') + """) + cov_st, cov_out = self.run_command_status("coverage run excepthook_throw.py") + py_st, py_out = self.run_command_status("python excepthook_throw.py") + if not env.JYTHON: + self.assertEqual(cov_st, py_st) + self.assertEqual(cov_st, 1) + + self.assertIn("in excepthook", py_out) + self.assertEqual(cov_out, py_out) + + class AliasedCommandTest(CoverageTest): """Tests of the version-specific command aliases.""" run_in_temp_dir = False + def setUp(self): + super(AliasedCommandTest, self).setUp() + if env.JYTHON: + self.skipTest("Coverage command names don't work on Jython") + def test_major_version_works(self): # "coverage2" works on py2 cmd = "coverage%d" % sys.version_info[0] @@ -779,6 +887,7 @@ class AliasedCommandTest(CoverageTest): def test_wrong_alias_doesnt_work(self): # "coverage3" doesn't work on py2 + assert sys.version_info[0] in [2, 3] # Let us know when Python 4 is out... badcmd = "coverage%d" % (5 - sys.version_info[0]) out = self.run_command(badcmd) self.assertNotIn("Code coverage for Python", out) @@ -850,125 +959,40 @@ class FailUnderTest(CoverageTest): ) def test_report(self): - st, _ = self.run_command_status("coverage report --fail-under=42") - self.assertEqual(st, 0) st, _ = self.run_command_status("coverage report --fail-under=43") self.assertEqual(st, 0) st, _ = self.run_command_status("coverage report --fail-under=44") self.assertEqual(st, 2) - def test_html_report(self): - st, _ = self.run_command_status("coverage html --fail-under=42") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage html --fail-under=43") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage html --fail-under=44") - self.assertEqual(st, 2) - - def test_xml_report(self): - st, _ = self.run_command_status("coverage xml --fail-under=42") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage xml --fail-under=43") - self.assertEqual(st, 0) - st, _ = self.run_command_status("coverage xml --fail-under=44") - self.assertEqual(st, 2) - - def test_fail_under_in_config(self): - self.make_file(".coveragerc", "[report]\nfail_under = 43\n") - st, _ = self.run_command_status("coverage report") - self.assertEqual(st, 0) - - self.make_file(".coveragerc", "[report]\nfail_under = 44\n") - st, _ = self.run_command_status("coverage report") - self.assertEqual(st, 2) - class FailUnderNoFilesTest(CoverageTest): """Test that nothing to report results in an error exit status.""" - def setUp(self): - super(FailUnderNoFilesTest, self).setUp() - self.make_file(".coveragerc", "[report]\nfail_under = 99\n") - def test_report(self): + self.make_file(".coveragerc", "[report]\nfail_under = 99\n") st, out = self.run_command_status("coverage report") self.assertIn('No data to report.', out) self.assertEqual(st, 1) - def test_xml(self): - st, out = self.run_command_status("coverage xml") - self.assertIn('No data to report.', out) - self.assertEqual(st, 1) - - def test_html(self): - st, out = self.run_command_status("coverage html") - self.assertIn('No data to report.', out) - self.assertEqual(st, 1) - class FailUnderEmptyFilesTest(CoverageTest): """Test that empty files produce the proper fail_under exit status.""" - def setUp(self): - super(FailUnderEmptyFilesTest, self).setUp() - + def test_report(self): self.make_file(".coveragerc", "[report]\nfail_under = 99\n") self.make_file("empty.py", "") st, _ = self.run_command_status("coverage run empty.py") self.assertEqual(st, 0) - - def test_report(self): st, _ = self.run_command_status("coverage report") self.assertEqual(st, 2) - def test_xml(self): - st, _ = self.run_command_status("coverage xml") - self.assertEqual(st, 2) - - def test_html(self): - st, _ = self.run_command_status("coverage html") - self.assertEqual(st, 2) - - -class FailUnder100Test(CoverageTest): - """Tests of the --fail-under switch.""" - - def test_99_8(self): - self.make_file("ninety_nine_eight.py", - "".join("v{i} = {i}\n".format(i=i) for i in range(498)) + - "if v0 > 498:\n v499 = 499\n" - ) - st, _ = self.run_command_status("coverage run ninety_nine_eight.py") - self.assertEqual(st, 0) - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual( - self.last_line_squeezed(out), - "ninety_nine_eight.py 500 1 99%" - ) - - st, _ = self.run_command_status("coverage report --fail-under=100") - self.assertEqual(st, 2) - - - def test_100(self): - self.make_file("one_hundred.py", - "".join("v{i} = {i}\n".format(i=i) for i in range(500)) - ) - st, _ = self.run_command_status("coverage run one_hundred.py") - self.assertEqual(st, 0) - st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual( - self.last_line_squeezed(out), - "one_hundred.py 500 0 100%" - ) - - st, _ = self.run_command_status("coverage report --fail-under=100") - self.assertEqual(st, 0) - class UnicodeFilePathsTest(CoverageTest): """Tests of using non-ascii characters in the names of files.""" + def setUp(self): + super(UnicodeFilePathsTest, self).setUp() + if env.JYTHON: + self.skipTest("Jython doesn't like accented file names") + def test_accented_dot_py(self): # Make a file with a non-ascii character in the filename. self.make_file(u"h\xe2t.py", "print('accented')") @@ -998,7 +1022,6 @@ class UnicodeFilePathsTest(CoverageTest): ) if env.PY2: - # pylint: disable=redefined-variable-type report_expected = report_expected.encode(output_encoding()) out = self.run_command("coverage report") @@ -1037,7 +1060,6 @@ class UnicodeFilePathsTest(CoverageTest): ) if env.PY2: - # pylint: disable=redefined-variable-type report_expected = report_expected.encode(output_encoding()) out = self.run_command("coverage report") @@ -1046,17 +1068,35 @@ class UnicodeFilePathsTest(CoverageTest): def possible_pth_dirs(): """Produce a sequence of directories for trying to write .pth files.""" - # First look through sys.path, and we find a .pth file, then it's a good + # First look through sys.path, and if we find a .pth file, then it's a good # place to put ours. - for d in sys.path: - g = glob.glob(os.path.join(d, "*.pth")) - if g: - yield d + for pth_dir in sys.path: # pragma: part covered + pth_files = glob.glob(os.path.join(pth_dir, "*.pth")) + if pth_files: + yield pth_dir # If we're still looking, then try the Python library directory. # https://bitbucket.org/ned/coveragepy/issue/339/pth-test-malfunctions - import distutils.sysconfig # pylint: disable=import-error - yield distutils.sysconfig.get_python_lib() + yield distutils.sysconfig.get_python_lib() # pragma: cant happen + + +def find_writable_pth_directory(): + """Find a place to write a .pth file.""" + for pth_dir in possible_pth_dirs(): # pragma: part covered + try_it = os.path.join(pth_dir, "touch_{0}.it".format(WORKER)) + with open(try_it, "w") as f: + try: + f.write("foo") + except (IOError, OSError): # pragma: cant happen + continue + + os.remove(try_it) + return pth_dir + + return None # pragma: cant happen + +WORKER = os.environ.get('PYTEST_XDIST_WORKER', '') +PTH_DIR = find_writable_pth_directory() class ProcessCoverageMixin(object): @@ -1064,19 +1104,14 @@ class ProcessCoverageMixin(object): def setUp(self): super(ProcessCoverageMixin, self).setUp() - # Find a place to put a .pth file. + + # Create the .pth file. + self.assertTrue(PTH_DIR) pth_contents = "import coverage; coverage.process_startup()\n" - for pth_dir in possible_pth_dirs(): # pragma: part covered - pth_path = os.path.join(pth_dir, "subcover.pth") - with open(pth_path, "w") as pth: - try: - pth.write(pth_contents) - self.pth_path = pth_path - break - except (IOError, OSError): # pragma: not covered - pass - else: # pragma: not covered - raise Exception("Couldn't find a place for the .pth file") + pth_path = os.path.join(PTH_DIR, "subcover_{0}.pth".format(WORKER)) + with open(pth_path, "w") as pth: + pth.write(pth_contents) + self.pth_path = pth_path self.addCleanup(os.remove, self.pth_path) @@ -1095,11 +1130,12 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): """) # sub.py will write a few lines. self.make_file("sub.py", """\ - with open("out.txt", "w") as f: - f.write("Hello, world!\\n") + f = open("out.txt", "w") + f.write("Hello, world!\\n") + f.close() """) - def test_subprocess_with_pth_files(self): # pragma: not covered + def test_subprocess_with_pth_files(self): # pragma: no metacov if env.METACOV: self.skipTest("Can't test sub-process pth file suppport during metacoverage") @@ -1124,9 +1160,9 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".mycovdata") data = coverage.CoverageData() data.read_file(".mycovdata") - self.assertEqual(data.line_counts()['sub.py'], 2) + self.assertEqual(data.line_counts()['sub.py'], 3) - def test_subprocess_with_pth_files_and_parallel(self): # pragma: not covered + def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov # https://bitbucket.org/ned/coveragepy/issues/492/subprocess-coverage-strange-detection-of if env.METACOV: self.skipTest("Can't test sub-process pth file suppport during metacoverage") @@ -1148,12 +1184,12 @@ class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): self.assert_exists(".coverage") data = coverage.CoverageData() data.read_file(".coverage") - self.assertEqual(data.line_counts()['sub.py'], 2) + self.assertEqual(data.line_counts()['sub.py'], 3) # assert that there are *no* extra data files left over after a combine data_files = glob.glob(os.getcwd() + '/.coverage*') - self.assertEquals(len(data_files), 1, - "Expected only .coverage after combine, looks like there are " + \ + self.assertEqual(len(data_files), 1, + "Expected only .coverage after combine, looks like there are " "extra data files that were not cleaned up: %r" % data_files) @@ -1173,7 +1209,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): def assert_pth_and_source_work_together( self, dashm, package, source - ): # pragma: not covered + ): # pragma: no metacov """Run the test for a particular combination of factors. The arguments are all strings: @@ -1205,14 +1241,17 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): # Main will run sub.py. self.make_file(path("main.py"), """\ import %s - if True: pass + a = 2 + b = 3 """ % fullname('sub')) if package: self.make_file(path("__init__.py"), "") # sub.py will write a few lines. self.make_file(path("sub.py"), """\ - with open("out.txt", "w") as f: - f.write("Hello, world!") + # Avoid 'with' so Jython can play along. + f = open("out.txt", "w") + f.write("Hello, world!") + f.close() """) self.make_file("coverage.ini", """\ [run] @@ -1237,7 +1276,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): data.read_file(".coverage") summary = data.line_counts() print(summary) - self.assertEqual(summary[source + '.py'], 2) + self.assertEqual(summary[source + '.py'], 3) self.assertEqual(len(summary), 1) def test_dashm_main(self): @@ -1263,9 +1302,3 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): def test_script_pkg_sub(self): self.assert_pth_and_source_work_together('', 'pkg', 'sub') - - -def remove_matching_lines(text, pat): - """Return `text` with all lines matching `pat` removed.""" - lines = [l for l in text.splitlines(True) if not re.search(pat, l)] - return "".join(lines) diff --git a/tests/test_python.py b/tests/test_python.py index ee1e1f9..9027aa6 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -6,7 +6,10 @@ import os import sys -from coverage.python import get_zip_bytes +import pytest + +from coverage import env +from coverage.python import get_zip_bytes, source_for_file from tests.coveragetest import CoverageTest @@ -28,3 +31,31 @@ class GetZipBytesTest(CoverageTest): self.assertIn('All OK', zip_text) # Run the code to see that we really got it encoded properly. __import__("encoded_"+encoding) + + +def test_source_for_file(tmpdir): + path = tmpdir.join("a.py") + src = str(path) + assert source_for_file(src) == src + assert source_for_file(src + 'c') == src + assert source_for_file(src + 'o') == src + unknown = src + 'FOO' + assert source_for_file(unknown) == unknown + + +@pytest.mark.skipif(not env.WINDOWS, reason="not windows") +def test_source_for_file_windows(tmpdir): + path = tmpdir.join("a.py") + src = str(path) + + # On windows if a pyw exists, it is an acceptable source + path_windows = tmpdir.ensure("a.pyw") + assert str(path_windows) == source_for_file(src + 'c') + + # If both pyw and py exist, py is preferred + path.ensure(file=True) + assert source_for_file(src + 'c') == src + + +def test_source_for_file_jython(): + assert source_for_file("a$py.class") == "a.py" diff --git a/tests/test_results.py b/tests/test_results.py index 54c2f6d..280694d 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -3,7 +3,10 @@ """Tests for coverage.py's results analysis.""" -from coverage.results import Numbers +import pytest + +from coverage.results import Numbers, should_fail_under + from tests.coveragetest import CoverageTest @@ -40,6 +43,8 @@ class NumbersTest(CoverageTest): self.assertAlmostEqual(n3.pc_covered, 86.666666666) def test_pc_covered_str(self): + # Numbers._precision is a global, which is bad. + Numbers.set_precision(0) n0 = Numbers(n_files=1, n_statements=1000, n_missing=0) n1 = Numbers(n_files=1, n_statements=1000, n_missing=1) n999 = Numbers(n_files=1, n_statements=1000, n_missing=999) @@ -50,7 +55,7 @@ class NumbersTest(CoverageTest): self.assertEqual(n1000.pc_covered_str, "0") def test_pc_covered_str_precision(self): - assert Numbers._precision == 0 + # Numbers._precision is a global, which is bad. Numbers.set_precision(1) n0 = Numbers(n_files=1, n_statements=10000, n_missing=0) n1 = Numbers(n_files=1, n_statements=10000, n_missing=1) @@ -71,3 +76,23 @@ class NumbersTest(CoverageTest): n_branches=10, n_missing_branches=3, n_partial_branches=1000, ) self.assertEqual(n.ratio_covered, (160, 210)) + + +@pytest.mark.parametrize("total, fail_under, result", [ + # fail_under==0 means anything is fine! + (0, 0, False), + (0.001, 0, False), + # very small fail_under is possible to fail. + (0.001, 0.01, True), + # Rounding should work properly. + (42.1, 42, False), + (42.1, 43, True), + (42.857, 42, False), + (42.857, 43, False), + (42.857, 44, True), + # Values near 100 should only be treated as 100 if they are 100. + (99.8, 100, True), + (100.0, 100, False), +]) +def test_should_fail_under(total, fail_under, result): + assert should_fail_under(total, fail_under) == result diff --git a/tests/test_setup.py b/tests/test_setup.py new file mode 100644 index 0000000..6533418 --- /dev/null +++ b/tests/test_setup.py @@ -0,0 +1,47 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Tests of miscellaneous stuff.""" + +import sys + +import coverage + +from tests.coveragetest import CoverageTest + + +class SetupPyTest(CoverageTest): + """Tests of setup.py""" + + run_in_temp_dir = False + + def setUp(self): + super(SetupPyTest, self).setUp() + # Force the most restrictive interpretation. + self.set_environ('LC_ALL', 'C') + + def test_metadata(self): + status, output = self.run_command_status( + "python setup.py --description --version --url --author" + ) + self.assertEqual(status, 0) + out = output.splitlines() + self.assertIn("measurement", out[0]) + self.assertEqual(out[1], coverage.__version__) + self.assertEqual(out[2], coverage.__url__) + self.assertIn("Ned Batchelder", out[3]) + + def test_more_metadata(self): + # Let's be sure we pick up our own setup.py + # CoverageTest restores the original sys.path for us. + sys.path.insert(0, '') + from setup import setup_args + + classifiers = setup_args['classifiers'] + self.assertGreater(len(classifiers), 7) + self.assert_starts_with(classifiers[-1], "Development Status ::") + + long_description = setup_args['long_description'].splitlines() + self.assertGreater(len(long_description), 7) + self.assertNotEqual(long_description[0].strip(), "") + self.assertNotEqual(long_description[-1].strip(), "") diff --git a/tests/test_summary.py b/tests/test_summary.py index bda6568..7c9f4c1 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -1,4 +1,4 @@ -# coding: utf8 +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt @@ -361,6 +361,27 @@ class SummaryTest(CoverageTest): squeezed = self.squeezed_lines(report) self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + def test_report_skip_covered_longfilename(self): + self.make_file("long_______________filename.py", """ + def foo(): + pass + foo() + """) + out = self.run_command("coverage run --branch long_______________filename.py") + self.assertEqual(out, "") + report = self.report_from_command("coverage report --skip-covered") + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # + # 1 file skipped due to complete coverage. + + self.assertEqual(self.line_count(report), 4, report) + lines = self.report_lines(report) + self.assertEqual(lines[0], "Name Stmts Miss Branch BrPart Cover") + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") @@ -381,22 +402,25 @@ class SummaryTest(CoverageTest): self.make_file("mycode.py", "This isn't python at all!") report = self.report_from_command("coverage report mycode.py") + # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- - # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # No data to report. - last = self.squeezed_lines(report)[-2] + errmsg = self.squeezed_lines(report)[0] # The actual file name varies run to run. - last = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", last) + errmsg = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", errmsg) # The actual error message varies version to version - last = re.sub(r": '.*' at", ": 'error' at", last) + errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) self.assertEqual( - last, + errmsg, "mycode.py NotPython: Couldn't parse 'mycode.py' as Python source: 'error' at line 1" ) def test_accenteddotpy_not_python(self): + if env.JYTHON: + self.skipTest("Jython doesn't like accented file names") + # We run a .py file with a non-ascii name, and when reporting, we can't # parse it as Python. We should get an error message in the report. @@ -405,24 +429,23 @@ class SummaryTest(CoverageTest): self.make_file(u"accented\xe2.py", "This isn't python at all!") report = self.report_from_command(u"coverage report accented\xe2.py") + # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- - # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # No data to report. - last = self.squeezed_lines(report)[-2] + errmsg = self.squeezed_lines(report)[0] # The actual file name varies run to run. - last = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", last) + errmsg = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", errmsg) # The actual error message varies version to version - last = re.sub(r": '.*' at", ": 'error' at", last) + errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) expected = ( u"accented\xe2.py NotPython: " u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1" ) if env.PY2: - # pylint: disable=redefined-variable-type expected = expected.encode(output_encoding()) - self.assertEqual(last, expected) + self.assertEqual(errmsg, expected) def test_dotpy_not_python_ignored(self): # We run a .py file, and when reporting, we can't parse it as Python, @@ -564,7 +587,7 @@ class SummaryTest(CoverageTest): # Python 3 puts the .pyc files in a __pycache__ directory, and will # not import from there without source. It will import a .pyc from # the source location though. - if not os.path.exists("mod.pyc"): + if env.PY3 and not env.JYTHON: pycs = glob.glob("__pycache__/mod.*.pyc") self.assertEqual(len(pycs), 1) os.rename(pycs[0], "mod.pyc") @@ -656,7 +679,7 @@ class TestSummaryReporterConfiguration(CoverageTest): HERE = os.path.dirname(__file__) LINES_1 = { - os.path.join(HERE, "test_api.py"): dict.fromkeys(range(300)), + os.path.join(HERE, "test_api.py"): dict.fromkeys(range(400)), os.path.join(HERE, "test_backward.py"): dict.fromkeys(range(20)), os.path.join(HERE, "test_coverage.py"): dict.fromkeys(range(15)), } @@ -738,6 +761,14 @@ class TestSummaryReporterConfiguration(CoverageTest): report = self.get_summary_text(data, opts) self.assert_ordering(report, "test_backward.py", "test_coverage.py", "test_api.py") + def test_sort_report_by_missing(self): + # Sort the text report by the Missing column. + data = self.get_coverage_data(self.LINES_1) + opts = CoverageConfig() + opts.from_args(sort='Miss') + report = self.get_summary_text(data, opts) + self.assert_ordering(report, "test_backward.py", "test_api.py", "test_coverage.py") + def test_sort_report_by_cover(self): # Sort the text report by the Cover column. data = self.get_coverage_data(self.LINES_1) diff --git a/tests/test_templite.py b/tests/test_templite.py index 1df942e..bcc65f9 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -1,4 +1,4 @@ -# -*- coding: utf8 -*- +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt diff --git a/tests/test_testing.py b/tests/test_testing.py index c5858bf..05bf0c9 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -8,11 +8,15 @@ import datetime import os import sys +import pytest + import coverage -from coverage.backunittest import TestCase +from coverage.backunittest import TestCase, unittest from coverage.files import actual_path +from coverage.misc import StopEverything -from tests.coveragetest import CoverageTest +from tests.coveragetest import CoverageTest, convert_skip_exceptions +from tests.helpers import CheckUniqueFilenames, re_lines, re_line class TestingTest(TestCase): @@ -110,14 +114,38 @@ class CoverageTestTest(CoverageTest): with self.assert_warnings(cov, ["Not me"]): cov._warn("Hello there!") + # Try checking a warning that shouldn't appear: happy case. + with self.assert_warnings(cov, ["Hi"], not_warnings=["Bye"]): + cov._warn("Hi") + + # But it should fail if the unexpected warning does appear. + warn_regex = r"Found warning 'Bye' in \['Hi', 'Bye'\]" + with self.assertRaisesRegex(AssertionError, warn_regex): + with self.assert_warnings(cov, ["Hi"], not_warnings=["Bye"]): + cov._warn("Hi") + cov._warn("Bye") + # assert_warnings shouldn't hide a real exception. with self.assertRaises(ZeroDivisionError): with self.assert_warnings(cov, ["Hello there!"]): raise ZeroDivisionError("oops") + def test_assert_no_warnings(self): + cov = coverage.Coverage() + + # Happy path: no warnings. + with self.assert_warnings(cov, []): + pass + + # If you said there would be no warnings, and there were, fail! + warn_regex = r"Unexpected warnings: \['Watch out!'\]" + with self.assertRaisesRegex(AssertionError, warn_regex): + with self.assert_warnings(cov, []): + cov._warn("Watch out!") + def test_sub_python_is_this_python(self): # Try it with a Python command. - os.environ['COV_FOOBAR'] = 'XYZZY' + self.set_environ('COV_FOOBAR', 'XYZZY') self.make_file("showme.py", """\ import os, sys print(sys.executable) @@ -130,17 +158,97 @@ class CoverageTestTest(CoverageTest): self.assertEqual(out[2], 'XYZZY') # Try it with a "coverage debug sys" command. - out = self.run_command("coverage debug sys").splitlines() - # "environment: COV_FOOBAR = XYZZY" or "COV_FOOBAR = XYZZY" - executable = next(l for l in out if "executable:" in l) # pragma: part covered + out = self.run_command("coverage debug sys") + + executable = re_line(out, "executable:") executable = executable.split(":", 1)[1].strip() - self.assertTrue(same_python_executable(executable, sys.executable)) - environ = next(l for l in out if "COV_FOOBAR" in l) # pragma: part covered + self.assertTrue(_same_python_executable(executable, sys.executable)) + + # "environment: COV_FOOBAR = XYZZY" or "COV_FOOBAR = XYZZY" + environ = re_line(out, "COV_FOOBAR") _, _, environ = environ.rpartition(":") self.assertEqual(environ.strip(), "COV_FOOBAR = XYZZY") -def same_python_executable(e1, e2): +class CheckUniqueFilenamesTest(CoverageTest): + """Tests of CheckUniqueFilenames.""" + + run_in_temp_dir = False + + class Stub(object): + """A stand-in for the class we're checking.""" + def __init__(self, x): + self.x = x + + def method(self, filename, a=17, b="hello"): + """The method we'll wrap, with args to be sure args work.""" + return (self.x, filename, a, b) + + def test_detect_duplicate(self): + stub = self.Stub(23) + CheckUniqueFilenames.hook(stub, "method") + + # Two method calls with different names are fine. + assert stub.method("file1") == (23, "file1", 17, "hello") + assert stub.method("file2", 1723, b="what") == (23, "file2", 1723, "what") + + # A duplicate file name trips an assertion. + with self.assertRaises(AssertionError): + stub.method("file1") + + +@pytest.mark.parametrize("text, pat, result", [ + ("line1\nline2\nline3\n", "line", "line1\nline2\nline3\n"), + ("line1\nline2\nline3\n", "[13]", "line1\nline3\n"), + ("line1\nline2\nline3\n", "X", ""), +]) +def test_re_lines(text, pat, result): + assert re_lines(text, pat) == result + +@pytest.mark.parametrize("text, pat, result", [ + ("line1\nline2\nline3\n", "line", ""), + ("line1\nline2\nline3\n", "[13]", "line2\n"), + ("line1\nline2\nline3\n", "X", "line1\nline2\nline3\n"), +]) +def test_re_lines_inverted(text, pat, result): + assert re_lines(text, pat, match=False) == result + +@pytest.mark.parametrize("text, pat, result", [ + ("line1\nline2\nline3\n", "2", "line2"), +]) +def test_re_line(text, pat, result): + assert re_line(text, pat) == result + +@pytest.mark.parametrize("text, pat", [ + ("line1\nline2\nline3\n", "line"), # too many matches + ("line1\nline2\nline3\n", "X"), # no matches +]) +def test_re_line_bad(text, pat): + with pytest.raises(AssertionError): + re_line(text, pat) + + +def test_convert_skip_exceptions(): + @convert_skip_exceptions + def some_method(ret=None, exc=None): + """Be like a test case.""" + if exc: + raise exc("yikes!") + return ret + + # Normal flow is normal. + assert some_method(ret=[17, 23]) == [17, 23] + + # Exceptions are raised normally. + with pytest.raises(ValueError): + some_method(exc=ValueError) + + # But a StopEverything becomes a SkipTest. + with pytest.raises(unittest.SkipTest): + some_method(exc=StopEverything) + + +def _same_python_executable(e1, e2): """Determine if `e1` and `e2` refer to the same Python executable. Either path could include symbolic links. The two paths might not refer diff --git a/tests/test_version.py b/tests/test_version.py new file mode 100644 index 0000000..eb8de87 --- /dev/null +++ b/tests/test_version.py @@ -0,0 +1,39 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +"""Tests of version.py.""" + +import coverage +from coverage.version import _make_url, _make_version + +from tests.coveragetest import CoverageTest + + +class VersionTest(CoverageTest): + """Tests of version.py""" + + run_in_temp_dir = False + + def test_version_info(self): + # Make sure we didn't screw up the version_info tuple. + self.assertIsInstance(coverage.version_info, tuple) + self.assertEqual([type(d) for d in coverage.version_info], [int, int, int, str, int]) + self.assertIn(coverage.version_info[3], ['alpha', 'beta', 'candidate', 'final']) + + def test_make_version(self): + self.assertEqual(_make_version(4, 0, 0, 'alpha', 0), "4.0a0") + self.assertEqual(_make_version(4, 0, 0, 'alpha', 1), "4.0a1") + self.assertEqual(_make_version(4, 0, 0, 'final', 0), "4.0") + self.assertEqual(_make_version(4, 1, 2, 'beta', 3), "4.1.2b3") + self.assertEqual(_make_version(4, 1, 2, 'final', 0), "4.1.2") + self.assertEqual(_make_version(5, 10, 2, 'candidate', 7), "5.10.2rc7") + + def test_make_url(self): + self.assertEqual( + _make_url(4, 0, 0, 'final', 0), + "https://coverage.readthedocs.io" + ) + self.assertEqual( + _make_url(4, 1, 2, 'beta', 3), + "https://coverage.readthedocs.io/en/coverage-4.1.2b3" + ) diff --git a/tests/test_xml.py b/tests/test_xml.py index dd14b92..c3493e7 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -1,3 +1,4 @@ +# coding: utf-8 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt @@ -8,11 +9,13 @@ import os.path import re import coverage +from coverage.backward import import_local_file from coverage.files import abs_file from tests.coveragetest import CoverageTest from tests.goldtest import CoverageGoldTest from tests.goldtest import change_dir, compare +from tests.helpers import re_line, re_lines class XmlTestHelpers(CoverageTest): @@ -165,8 +168,8 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): self.make_file("also/over/there/bar.py", "b = 2") cov = coverage.Coverage(source=["src/main", "also/over/there", "not/really"]) cov.start() - mod_foo = self.import_local_file("foo", "src/main/foo.py") # pragma: nested - mod_bar = self.import_local_file("bar", "also/over/there/bar.py") # pragma: nested + mod_foo = import_local_file("foo", "src/main/foo.py") # pragma: nested + mod_bar = import_local_file("bar", "also/over/there/bar.py") # pragma: nested cov.stop() # pragma: nested cov.xml_report([mod_foo, mod_bar], outfile="-") xml = self.stdout() @@ -184,6 +187,14 @@ class XmlReportTest(XmlTestHelpers, CoverageTest): xml ) + def test_nonascii_directory(self): + # https://bitbucket.org/ned/coveragepy/issues/573/cant-generate-xml-report-if-some-source + self.make_file("테스트/program.py", "a = 1") + with change_dir("테스트"): + cov = coverage.Coverage() + self.start_import_stop(cov, "program") + cov.xml_report() + class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """Tests about the package structure reported in the coverage.xml file.""" @@ -194,7 +205,7 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): cov.xml_report(outfile="-") packages_and_classes = re_lines(self.stdout(), r"<package |<class ") scrubs = r' branch-rate="0"| complexity="0"| line-rate="[\d.]+"' - return clean("".join(packages_and_classes), scrubs) + return clean(packages_and_classes, scrubs) def assert_package_and_class_tags(self, cov, result): """Check the XML package and class tags from `cov` match `result`.""" @@ -282,19 +293,6 @@ class XmlPackageStructureTest(XmlTestHelpers, CoverageTest): """) -def re_lines(text, pat): - """Return a list of lines that match `pat` in the string `text`.""" - lines = [l for l in text.splitlines(True) if re.search(pat, l)] - return lines - - -def re_line(text, pat): - """Return the one line in `text` that matches regex `pat`.""" - lines = re_lines(text, pat) - assert len(lines) == 1 - return lines[0] - - def clean(text, scrub=None): """Clean text to prepare it for comparison. diff --git a/tox-new.ini b/tox-new.ini index aaca6a4..bc5f041 100644 --- a/tox-new.ini +++ b/tox-new.ini @@ -51,13 +51,3 @@ basepython = pypy2.6 [testenv:pypy3_24] basepython = pypy3-2.4 - - -# Yes, pep8 will read its settings from tox.ini! -[pep8] -# E265: block comment should start with '# ' -# E301 expected 1 blank line, found 0 -# E401 multiple imports on one line -# The rest are the default ignored warnings. -ignore = E265,E123,E133,E226,E241,E242,E301,E401 -max-line-length = 100 @@ -2,7 +2,7 @@ # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt [tox] -envlist = py{26,27,33,34,35,36}, pypy{40,54,3_24,3_52}, doc +envlist = py{26,27,33,34,35,36}, pypy{2,3}, jython, doc, lint skip_missing_interpreters = True [testenv] @@ -10,18 +10,16 @@ usedevelop = True deps = # https://requires.io/github/nedbat/coveragepy/requirements/ - pip==8.1.2 - nose==1.3.7 + -rrequirements/pytest.pip + pip==9.0.1 mock==2.0.0 - PyContracts==1.7.9 - unittest-mixins==1.1.1 - #-egit+/Users/ned/unittest_mixins#egg=unittest-mixins==0.0 + PyContracts==1.7.15 + unittest-mixins==1.3 + #-e/Users/ned/unittest_mixins py26: unittest2==1.1.0 - py{27,33,34,35,36}: gevent==1.1.2 - py{26,27,33,34,35,36}: eventlet==0.19.0 - py{26,27,33,34,35,36}: greenlet==0.4.10 - # setuptools no longer supports Python 3.2 - pypy3_{24,52}: setuptools==21.1.0 + py{27,33,34,35,36}: gevent==1.2.1 + py{26,27,33,34,35,36}: eventlet==0.21.0 + py{26,27,33,34,35,36}: greenlet==0.4.12 # Windows can't update the pip version with pip running, so use Python # to install things. @@ -29,9 +27,8 @@ install_command = python -m pip install -U {opts} {packages} passenv = * setenv = - pypy,pypy{40,54,3_24,3_52}: COVERAGE_NO_EXTENSION=no C extension under PyPy - # Something (pip? setuptools?) chatters about 3.2 support going away. - pypy3_24: PYTHONWARNINGS=ignore:::pkg_resources + pypy,pypy{2,3}: COVERAGE_NO_CTRACER=no C extension under PyPy + jython: COVERAGE_NO_CTRACER=no C extension under Jython commands = python setup.py --quiet clean develop @@ -41,6 +38,13 @@ commands = # Remove the C extension so that we can test the PyTracer python igor.py zip_mods install_egg remove_extension + # When running parallel tests, many processes might all try to import the + # same modules at once. This should be safe, but especially on Python 3.3, + # this caused a number of test failures trying to import usepkgs. To + # prevent the race condition, pre-compile the tests/modules directory. + py33: python -m compileall -q -f tests/modules + py33: python -c "import time; time.sleep(1.1)" + # Test with the PyTracer python igor.py test_with_tracer py {posargs} @@ -56,17 +60,14 @@ install_command = python -m pip.__main__ install -U {opts} {packages} # the other environments... basepython = pypy -[testenv:pypy3_24] -basepython = pypy3-2.4 - -[testenv:pypy40] -basepython = pypy4.0 +[testenv:pypy2] +basepython = pypy2 -[testenv:pypy54] -basepython = pypy5.4 +[testenv:pypy3] +basepython = pypy3 -[testenv:pypy3_52] -basepython = pypy3-5.2 +[testenv:jython] +basepython = jython [testenv:doc] # Build the docs so we know if they are successful. We build twice: once with @@ -74,18 +75,19 @@ basepython = pypy3-5.2 # return. deps = -rdoc/requirements.pip commands = - doc8 -q doc CHANGES.rst README.rst + doc8 -q --ignore-path doc/_build doc CHANGES.rst README.rst + sphinx-build -b html -aqE doc doc/_build/html + rst2html.py --strict README.rst doc/_build/trash sphinx-build -b html -b linkcheck -aEnq doc doc/_build/html sphinx-build -b html -b linkcheck -aEnQW doc doc/_build/html - rst2html.py --strict CHANGES.rst doc/_build/trash - rst2html.py --strict README.rst doc/_build/trash -# Yes, pep8 will read its settings from tox.ini! -[pep8] -# E265 block comment should start with '# ' -# E266 too many leading '#' for block comment -# E301 expected 1 blank line, found 0 -# E401 multiple imports on one line -# The rest are the default ignored warnings. -ignore = E265,E266,E123,E133,E226,E241,E242,E301,E401 -max-line-length = 100 +[testenv:lint] +deps = -rrequirements/dev.pip + +setenv = + LINTABLE = coverage tests igor.py setup.py __main__.py + +commands = + python -m pylint --notes= {env:LINTABLE} + python -m tabnanny {env:LINTABLE} + python igor.py check_eol |