diff --git a/.editorconfig b/.editorconfig index bff28ab4b..f560af744 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # This file is for unifying the coding style for different editors and IDEs. # More information at http://EditorConfig.org @@ -33,3 +33,12 @@ max_line_length = 79 [Makefile] indent_style = tab indent_size = 8 + +[*,cover] +trim_trailing_whitespace = false + +[*.diff] +trim_trailing_whitespace = false + +[.git/*] +trim_trailing_whitespace = false diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..5e7ec8760 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: nedbat +tidelift: pypi/coverage diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..95d12bf78 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,25 @@ +--- +name: Bug report +about: Report a problem with coverage.py +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of the bug. + +**To Reproduce** +How can we reproduce the problem? Please *be specific*. Don't just link to a failing CI job. Answer the questions below: +1. What version of Python are you using? +1. What version of coverage.py are you using? The output of `coverage debug sys` is helpful. +1. What versions of what packages do you have installed? The output of `pip freeze` is helpful. +1. What code are you running? Give us a specific commit of a specific repo that we can check out. +1. What commands did you run? + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..433310b17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,16 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +# https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository + +blank_issues_enabled: false +contact_links: + - name: Frequently Asked Questions + url: https://coverage.readthedocs.io/en/latest/faq.html + about: Some common problems are described here. + - name: Testing in Python mailing list + url: http://lists.idyll.org/listinfo/testing-in-python + about: Ask questions about using coverage.py here. + - name: Tidelift security contact + url: https://tidelift.com/security + about: Please report security vulnerabilities here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..c44202ba6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for coverage.py +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context about the feature request here. diff --git a/.gitignore b/.gitignore index b952771a1..f8813653a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,19 +23,27 @@ dist htmlcov MANIFEST setuptools-*.egg -.tox* +.tox .noseids .cache .pytest_cache .hypothesis +.ruby-version # Stuff in the test directory. +covmain.zip zipmods.zip # Stuff in the doc directory. -_build -_spell -sample_html_beta +doc/_build +doc/_spell +doc/sample_html_beta + +# Build intermediaries. +tmp # Stuff in the ci directory. *.token + +# OS junk +.DS_Store diff --git a/.hgignore b/.hgignore deleted file mode 100644 index ce114b130..000000000 --- a/.hgignore +++ /dev/null @@ -1,42 +0,0 @@ -syntax: glob - -# Files that can appear anywhere in the tree. -*.pyc -*.pyo -*$py.class -*.pyd -*.so -*.bak -.coverage -.coverage.* -coverage.xml -.metacov -.metacov.* -*.swp - -# Stuff generated by editors. -.idea/ -.vimtags - -# Stuff in the root. -build -*.egg-info -dist -htmlcov -MANIFEST -setuptools-*.egg -.tox* -.noseids -.cache -.hypothesis - -# Stuff in the test directory. -zipmods.zip - -# Stuff in the doc directory. -_build -_spell -sample_html_beta - -# Stuff in the ci directory. -*.token diff --git a/.hgtags b/.hgtags deleted file mode 100644 index 8f9ad85cc..000000000 --- a/.hgtags +++ /dev/null @@ -1,67 +0,0 @@ -4105a4de000ef94bb1f80cecae7c93aaf282bdbc coverage-3.0b1 -79dd373074def79b3822ee9254307295bf991d16 coverage-3.0b2 -ad991769ad4f685089bacc594e689bab50a6db90 coverage-3.0b3 -9041c4e99c3f8181d35e47df6fd83ceedb1f47fc coverage-3.0 -483a54854358881c360c6a717709b45a5225bd69 coverage-3.0.1 -67f7698c6c0413cc335ed5af7dd981bcf0a126a4 coverage-3.1b1 -67f7698c6c0413cc335ed5af7dd981bcf0a126a4 coverage-3.1b1 -27b6be608b2be50b92b91610846117ab7c47831c coverage-3.1b1 -4d89e089a8f3964522b19d50b4e90be0f2e49b53 coverage-3.1 -524f15a1a176be1b9afb737ab1be99c972f8c928 coverage-3.2b1 -524f15a1a176be1b9afb737ab1be99c972f8c928 coverage-3.2b1 -9231251c603ef0d491b01799b3d1da7af54dd083 coverage-3.2b1 -9231251c603ef0d491b01799b3d1da7af54dd083 coverage-3.2b1 -da052544181e335a18b4141b8b7a47e8246aa9e8 coverage-3.2b1 -11f0fbec2bc9b22a5fd8687d8b94338a4a363e6c coverage-3.2b2 -d42ad49a0aaca90867644876948a7bdab0aa9970 coverage-3.2b3 -3e21d5dc7e9d9c64db56d84b245618ad5c1a4743 coverage-3.2b4 -e3a46ca12bda98e81329300994ca7573dc3af86e coverage-3.2 -e3a46ca12bda98e81329300994ca7573dc3af86e coverage-3.2 -2e1628da1ffce31ed5c58bcad657882de282c858 coverage-3.2 -f63624fedea89b2acb361e1d7d6c5375ebced2aa coverage-3.3 -91bf9589714b14ac4a2ab5995635736994b97348 coverage-3.3.1 -7983cc5256eafaa6c6c3cdb05266493b4c1f991a coverage-3.4b1 -fcc4407bece6ca1266c91734bfdc657b66e10290 coverage-3.4b2 -25f2c1579f1c9539b2c140b86f6c00cbfbf4c565 coverage-3.4 -b7db1026817a4ae98c58c561f6ef79e95d0e13f9 coverage-3.5b1 -7be6416f60b90b75d8c81e5369612b7f48dc26cd coverage-3.5 -9229ab7b00f9e6542b44e788916f4117bd27cfdb coverage-3.5.1b1 -465bedd8af1f79a61f6dda018f6aefe97a79ae4d coverage-3.5.1 -afc3c2ffdb323bf059f136c4c88199899ed93d36 coverage-3.5.2b1 -4eb95ddb88496eccf5f399ba69ca1769baf29dfd coverage-3.5.2 -5429bbb25e8f124f5401cce1effa3089dd792711 coverage-3.5.3 -3483c970350aedb2c8fbe33def962041628f0ba6 coverage-3.6b1 -44d084b18bf0b96f49964df344c1dcaee3802d2e coverage-3.6b2 -773375910ceaaebfce4b5badc584559bf139c862 coverage-3.6b3 -af20c6543c226fbc9deeba4a6d4114641374734a coverage-3.6 -092c0be6d011b6abf85b11476e3b548ed4d5b4c3 coverage-3.7 -41932e001f215955dff895a1fd2fe4133abb89dc coverage-3.7.1 -175fd36ea47ea668066965c80343e1de34f2a94a coverage-4.0a1 -3b97c7c3178f0b88f1ee98f4c58ad00ca3d8e3b4 coverage-4.0a2 -eec8e928880df1beafdf7d4bea87f784375b35d7 coverage-4.0a3 -989f42e9eb11b5864e5746cea675d95ff4cf645d coverage-4.0a4 -00a61f028fcb000ae2cbc77bfbe0ac4cfacfab65 coverage-4.0a5 -90debbdf56495e6c0422ceb5f53f8550d2ad86bf coverage-4.0a6 -825fb6db681e9cfb227f09adbee50b881a3380e2 coverage-4.0b1 -9ab0815227d29c03775f7f7ad6dba1b0d93db777 coverage-4.0b2 -82b0c5a85a7eb2156eebaa6b81b1f62fb4fe51b5 coverage-4.0b3 -3c3e507a247eb35251083b9528a99e50831c960f coverage-4.0 -8e727dc12de10fb8a302b04a8f2af3e00587889e coverage-4.0.1 -7428dab9307da4660878436fe71b696ca2048cf2 coverage-4.0.2 -5ab728b8fd55bd78352012a15a6541fa73dd9e2c coverage-4.0.3 -166fbccc94fb32e52a18508c5422ddae5d5184b9 coverage-4.1b1 -05377ccfe33a83068962a9010037db2d3ebdfa1a coverage-4.1b2 -4984b5b2273622a98d6e4b7de6461bbf6fae41e3 coverage-4.1b3 -1b57373355d1d0a2de258655c47412698d5f061a coverage-4.1 -c84cdae01ca45f85e396c65c20e0234e89ef1785 coverage-4.2b1 -d96ff67e4ef7d0c5e787ed5aafbc1b5a04f6e97a coverage-4.2 -e3e18c651fa059c909c25992d5b7a6db371de09b coverage-4.3 -6ecabe2f7a8d8676a05585bffe19f88b195698ae coverage-4.3.1 -dd2d866194d2eca05862230e6003c6e04fc2fdc0 coverage-4.3.2 -3714d7c42000deafd2fb2034fc8eee6a9ec4c280 coverage-4.3.3 -19d0627fca7a7e76c3c27df1d70ce7f536f1ff23 coverage-4.3.4 -30ab67f19960fafd836c89a08b5f7ee1510b8c03 coverage-4.4b1 -8ad6fa0931cd5eb9b7bb8e6dec6b066d2988fc8f coverage-4.4 -ed196840b79136f17ab493699ec83dcf7dbfe973 coverage-4.4.1 -b65ae46a6504b8d577e967bd3fdcfcaceec95528 coverage-4.4.2 -102b2250a123537e640cd014f5df281822e79cec coverage-4.5 diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..ed3737fbe --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,22 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +# +# ReadTheDocs configuration. +# See https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +sphinx: + builder: html + configuration: doc/conf.py + +# No other formats than HTML +formats: [] + +python: + version: 3.7 + install: + - requirements: doc/requirements.pip + - method: pip + path: . + system_packages: false diff --git a/.travis.yml b/.travis.yml index 5e28d48b0..f5e8fad19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,52 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +# # Tell Travis what to do -# https://travis-ci.org/nedbat/coveragepy +# https://travis-ci.com/nedbat/coveragepy +dist: xenial language: python -sudo: false +cache: pip + python: - - '2.6' - '2.7' - - '3.3' - - '3.4' - '3.5' - '3.6' - - 'pypy' + - '3.7' + - '3.8' + - 'pypy2.7-6.0' + - 'pypy3.5-6.0' + +# Only testing it for python3.8 on aarch64 platform, since it already has a lot +# of jobs to test and takes long time. +matrix: + include: + - python: 3.8 + arch: arm64 + env: + - COVERAGE_COVERAGE=no + - python: 3.8 + arch: arm64 + env: + - COVERAGE_COVERAGE=yes env: matrix: - COVERAGE_COVERAGE=no - COVERAGE_COVERAGE=yes -sudo: false - install: - pip install -r requirements/ci.pip + - pip freeze 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 -X gcov --file coverage.xml; fi + +after_script: + - | + if [[ $COVERAGE_COVERAGE == 'yes' ]]; then + python igor.py combine_html + pip install codecov + codecov -X gcov --file coverage.xml + fi diff --git a/.treerc b/.treerc index c36714231..34862ad4f 100644 --- a/.treerc +++ b/.treerc @@ -5,10 +5,12 @@ ignore = .hgtags build htmlcov - .tox* + html0 + .tox* .coverage* .metacov mock.py - *.min.js + *.min.js style.css + gold sample_html sample_html_beta *.so *.pyd *.gz *.zip diff --git a/CHANGES.rst b/CHANGES.rst index e4243b9b3..28c1f4cd7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,21 +1,650 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt ============================== -Change history for Coverage.py +Change history for coverage.py ============================== +These changes are listed in decreasing version number order. Note this can be +different from a strict chronological order when there are two branches in +development at the same time, such as 4.5.x and 5.0. + +This list is detailed and covers changes in each pre-release version. If you +want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. + + .. When updating the "Unreleased" header to a specific version, use this .. format. Don't forget the jump target: .. + .. .. _changes_981: .. - .. .. _changes_781: - .. - .. - .. Version 7.8.1 --- 2021-07-27 + .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- +.. _changes_521: + +Version 5.2.1 --- 2020-07-23 +---------------------------- + +- The dark mode HTML report still used light colors for the context listing, + making them unreadable (`issue 1009`_). This is now fixed. + +- The time stamp on the HTML report now includes the time zone. Thanks, Xie + Yanbo (`pull request 960`_). + +.. _pull request 960: https://github.com/nedbat/coveragepy/pull/960 +.. _issue 1009: https://github.com/nedbat/coveragepy/issues/1009 + + +.. _changes_52: + +Version 5.2 --- 2020-07-05 +-------------------------- + +- The HTML report has been redesigned by Vince Salvino. There is now a dark + mode, the code text is larger, and system sans serif fonts are used, in + addition to other small changes (`issue 858`_ and `pull request 931`_). + +- The ``coverage report`` and ``coverage html`` commands now accept a + ``--precision`` option to control the number of decimal points displayed. + Thanks, Teake Nutma (`pull request 982`_). + +- The ``coverage report`` and ``coverage html`` commands now accept a + ``--no-skip-covered`` option to negate ``--skip-covered``. Thanks, Anthony + Sottile (`issue 779`_ and `pull request 932`_). + +- The ``--skip-empty`` option is now available for the XML report, closing + `issue 976`_. + +- The ``coverage report`` command now accepts a ``--sort`` option to specify + how to sort the results. Thanks, Jerin Peter George (`pull request 1005`_). + +- If coverage fails due to the coverage total not reaching the ``--fail-under`` + value, it will now print a message making the condition clear. Thanks, + Naveen Yadav (`pull request 977`_). + +- TOML configuration files with non-ASCII characters would cause errors on + Windows (`issue 990`_). This is now fixed. + +- The output of ``--debug=trace`` now includes information about how the + ``--source`` option is being interpreted, and the module names being + considered. + +.. _pull request 931: https://github.com/nedbat/coveragepy/pull/931 +.. _pull request 932: https://github.com/nedbat/coveragepy/pull/932 +.. _pull request 977: https://github.com/nedbat/coveragepy/pull/977 +.. _pull request 982: https://github.com/nedbat/coveragepy/pull/982 +.. _pull request 1005: https://github.com/nedbat/coveragepy/pull/1005 +.. _issue 779: https://github.com/nedbat/coveragepy/issues/779 +.. _issue 858: https://github.com/nedbat/coveragepy/issues/858 +.. _issue 976: https://github.com/nedbat/coveragepy/issues/976 +.. _issue 990: https://github.com/nedbat/coveragepy/issues/990 + + +.. _changes_51: + +Version 5.1 --- 2020-04-12 +-------------------------- + +- The JSON report now includes counts of covered and missing branches. Thanks, + Salvatore Zagaria. + +- On Python 3.8, try-finally-return reported wrong branch coverage with + decorated async functions (`issue 964`_). This is now fixed. Thanks, Kjell + Braden. + +- The :meth:`~coverage.Coverage.get_option` and + :meth:`~coverage.Coverage.set_option` methods can now manipulate the + ``[paths]`` configuration setting. Thanks to Bernát Gábor for the fix for + `issue 967`_. + +.. _issue 964: https://github.com/nedbat/coveragepy/issues/964 +.. _issue 967: https://github.com/nedbat/coveragepy/issues/967 + + +.. _changes_504: + +Version 5.0.4 --- 2020-03-16 +---------------------------- + +- If using the ``[run] relative_files`` setting, the XML report will use + relative files in the ```` elements indicating the location of source + code. Closes `issue 948`_. + +- The textual summary report could report missing lines with negative line + numbers on PyPy3 7.1 (`issue 943`_). This is now fixed. + +- Windows wheels for Python 3.8 were incorrectly built, but are now fixed. + (`issue 949`_) + +- Updated Python 3.9 support to 3.9a4. + +- HTML reports couldn't be sorted if localStorage wasn't available. This is now + fixed: sorting works even though the sorting setting isn't retained. (`issue + 944`_ and `pull request 945`_). Thanks, Abdeali Kothari. + +.. _issue 943: https://github.com/nedbat/coveragepy/issues/943 +.. _issue 944: https://github.com/nedbat/coveragepy/issues/944 +.. _pull request 945: https://github.com/nedbat/coveragepy/pull/945 +.. _issue 948: https://github.com/nedbat/coveragepy/issues/948 +.. _issue 949: https://github.com/nedbat/coveragepy/issues/949 + + +.. _changes_503: + +Version 5.0.3 --- 2020-01-12 +---------------------------- + +- A performance improvement in 5.0.2 didn't work for test suites that changed + directory before combining data, causing "Couldn't use data file: no such + table: meta" errors (`issue 916`_). This is now fixed. + +- Coverage could fail to run your program with some form of "ModuleNotFound" or + "ImportError" trying to import from the current directory. This would happen + if coverage had been packaged into a zip file (for example, on Windows), or + was found indirectly (for example, by pyenv-virtualenv). A number of + different scenarios were described in `issue 862`_ which is now fixed. Huge + thanks to Agbonze O. Jeremiah for reporting it, and Alexander Waters and + George-Cristian Bîrzan for protracted debugging sessions. + +- Added the "premain" debug option. + +- Added SQLite compile-time options to the "debug sys" output. + +.. _issue 862: https://github.com/nedbat/coveragepy/issues/862 +.. _issue 916: https://github.com/nedbat/coveragepy/issues/916 + + +.. _changes_502: + +Version 5.0.2 --- 2020-01-05 +---------------------------- + +- Programs that used multiprocessing and changed directories would fail under + coverage. This is now fixed (`issue 890`_). A side effect is that debug + information about the config files read now shows absolute paths to the + files. + +- When running programs as modules (``coverage run -m``) with ``--source``, + some measured modules were imported before coverage starts. This resulted in + unwanted warnings ("Already imported a file that will be measured") and a + reduction in coverage totals (`issue 909`_). This is now fixed. + +- If no data was collected, an exception about "No data to report" could happen + instead of a 0% report being created (`issue 884`_). This is now fixed. + +- The handling of source files with non-encodable file names has changed. + Previously, if a file name could not be encoded as UTF-8, an error occurred, + as described in `issue 891`_. Now, those files will not be measured, since + their data would not be recordable. + +- A new warning ("dynamic-conflict") is issued if two mechanisms are trying to + change the dynamic context. Closes `issue 901`_. + +- ``coverage run --debug=sys`` would fail with an AttributeError. This is now + fixed (`issue 907`_). + +.. _issue 884: https://github.com/nedbat/coveragepy/issues/884 +.. _issue 890: https://github.com/nedbat/coveragepy/issues/890 +.. _issue 891: https://github.com/nedbat/coveragepy/issues/891 +.. _issue 901: https://github.com/nedbat/coveragepy/issues/901 +.. _issue 907: https://github.com/nedbat/coveragepy/issues/907 +.. _issue 909: https://github.com/nedbat/coveragepy/issues/909 + + +.. _changes_501: + +Version 5.0.1 --- 2019-12-22 +---------------------------- + +- If a 4.x data file is the cause of a "file is not a database" error, then use + a more specific error message, "Looks like a coverage 4.x data file, are you + mixing versions of coverage?" Helps diagnose the problems described in + `issue 886`_. + +- Measurement contexts and relative file names didn't work together, as + reported in `issue 899`_ and `issue 900`_. This is now fixed, thanks to + David Szotten. + +- When using ``coverage run --concurrency=multiprocessing``, all data files + should be named with parallel-ready suffixes. 5.0 mistakenly named the main + process' file with no suffix when using ``--append``. This is now fixed, + closing `issue 880`_. + +- Fixed a problem on Windows when the current directory is changed to a + different drive (`issue 895`_). Thanks, Olivier Grisel. + +- Updated Python 3.9 support to 3.9a2. + +.. _issue 880: https://github.com/nedbat/coveragepy/issues/880 +.. _issue 886: https://github.com/nedbat/coveragepy/issues/886 +.. _issue 895: https://github.com/nedbat/coveragepy/issues/895 +.. _issue 899: https://github.com/nedbat/coveragepy/issues/899 +.. _issue 900: https://github.com/nedbat/coveragepy/issues/900 + + +.. _changes_50: + +Version 5.0 --- 2019-12-14 +-------------------------- + +Nothing new beyond 5.0b2. + + +.. _changes_50b2: + +Version 5.0b2 --- 2019-12-08 +---------------------------- + +- An experimental ``[run] relative_files`` setting tells coverage to store + relative file names in the data file. This makes it easier to run tests in + one (or many) environments, and then report in another. It has not had much + real-world testing, so it may change in incompatible ways in the future. + +- When constructing a :class:`coverage.Coverage` object, `data_file` can be + specified as None to prevent writing any data file at all. In previous + versions, an explicit `data_file=None` argument would use the default of + ".coverage". Fixes `issue 871`_. + +- Python files run with ``-m`` now have ``__spec__`` defined properly. This + fixes `issue 745`_ (about not being able to run unittest tests that spawn + subprocesses), and `issue 838`_, which described the problem directly. + +- The ``[paths]`` configuration section is now ordered. If you specify more + than one list of patterns, the first one that matches will be used. Fixes + `issue 649`_. + +- The :func:`.coverage.numbits.register_sqlite_functions` function now also + registers `numbits_to_nums` for use in SQLite queries. Thanks, Simon + Willison. + +- Python 3.9a1 is supported. + +- Coverage.py has a mascot: :ref:`Sleepy Snake `. + +.. _issue 649: https://github.com/nedbat/coveragepy/issues/649 +.. _issue 745: https://github.com/nedbat/coveragepy/issues/745 +.. _issue 838: https://github.com/nedbat/coveragepy/issues/838 +.. _issue 871: https://github.com/nedbat/coveragepy/issues/871 + + +.. _changes_50b1: + +Version 5.0b1 --- 2019-11-11 +---------------------------- + +- The HTML and textual reports now have a ``--skip-empty`` option that skips + files with no statements, notably ``__init__.py`` files. Thanks, Reya B. + +- Configuration can now be read from `TOML`_ files. This requires installing + coverage.py with the ``[toml]`` extra. The standard "pyproject.toml" file + will be read automatically if no other configuration file is found, with + settings in the ``[tool.coverage.]`` namespace. Thanks to Frazer McLean for + implementation and persistence. Finishes `issue 664`_. + +- The ``[run] note`` setting has been deprecated. Using it will result in a + warning, and the note will not be written to the data file. The + corresponding :class:`.CoverageData` methods have been removed. + +- The HTML report has been reimplemented (no more table around the source + code). This allowed for a better presentation of the context information, + hopefully resolving `issue 855`_. + +- Added sqlite3 module version information to ``coverage debug sys`` output. + +- Asking the HTML report to show contexts (``[html] show_contexts=True`` or + ``coverage html --show-contexts``) will issue a warning if there were no + contexts measured (`issue 851`_). + +.. _TOML: https://github.com/toml-lang/toml#readme +.. _issue 664: https://github.com/nedbat/coveragepy/issues/664 +.. _issue 851: https://github.com/nedbat/coveragepy/issues/851 +.. _issue 855: https://github.com/nedbat/coveragepy/issues/855 + + +.. _changes_50a8: + +Version 5.0a8 --- 2019-10-02 +---------------------------- + +- The :class:`.CoverageData` API has changed how queries are limited to + specific contexts. Now you use :meth:`.CoverageData.set_query_context` to + set a single exact-match string, or :meth:`.CoverageData.set_query_contexts` + to set a list of regular expressions to match contexts. This changes the + command-line ``--contexts`` option to use regular expressions instead of + filename-style wildcards. + + +.. _changes_50a7: + +Version 5.0a7 --- 2019-09-21 +---------------------------- + +- Data can now be "reported" in JSON format, for programmatic use, as requested + in `issue 720`_. The new ``coverage json`` command writes raw and summarized + data to a JSON file. Thanks, Matt Bachmann. + +- Dynamic contexts are now supported in the Python tracer, which is important + for PyPy users. Closes `issue 846`_. + +- The compact line number representation introduced in 5.0a6 is called a + "numbits." The :mod:`coverage.numbits` module provides functions for working + with them. + +- The reporting methods used to permanently apply their arguments to the + configuration of the Coverage object. Now they no longer do. The arguments + affect the operation of the method, but do not persist. + +- A class named "test_something" no longer confuses the ``test_function`` + dynamic context setting. Fixes `issue 829`_. + +- Fixed an unusual tokenizing issue with backslashes in comments. Fixes + `issue 822`_. + +- ``debug=plugin`` didn't properly support configuration or dynamic context + plugins, but now it does, closing `issue 834`_. + +.. _issue 720: https://github.com/nedbat/coveragepy/issues/720 +.. _issue 822: https://github.com/nedbat/coveragepy/issues/822 +.. _issue 834: https://github.com/nedbat/coveragepy/issues/834 +.. _issue 829: https://github.com/nedbat/coveragepy/issues/829 +.. _issue 846: https://github.com/nedbat/coveragepy/issues/846 + + +.. _changes_50a6: + +Version 5.0a6 --- 2019-07-16 +---------------------------- + +- Reporting on contexts. Big thanks to Stephan Richter and Albertas Agejevas + for the contribution. + + - The ``--contexts`` option is available on the ``report`` and ``html`` + commands. It's a comma-separated list of shell-style wildcards, selecting + the contexts to report on. Only contexts matching one of the wildcards + will be included in the report. + + - The ``--show-contexts`` option for the ``html`` command adds context + information to each covered line. Hovering over the "ctx" marker at the + end of the line reveals a list of the contexts that covered the line. + +- Database changes: + + - Line numbers are now stored in a much more compact way. For each file and + context, a single binary string is stored with a bit per line number. This + greatly improves memory use, but makes ad-hoc use difficult. + + - Dynamic contexts with no data are no longer written to the database. + + - SQLite data storage is now faster. There's no longer a reason to keep the + JSON data file code, so it has been removed. + +- Changes to the :class:`.CoverageData` interface: + + - The new :meth:`.CoverageData.dumps` method serializes the data to a string, + and a corresponding :meth:`.CoverageData.loads` method reconstitutes this + data. The format of the data string is subject to change at any time, and + so should only be used between two installations of the same version of + coverage.py. + + - The :meth:`CoverageData constructor<.CoverageData.__init__>` has a new + argument, `no_disk` (default: False). Setting it to True prevents writing + any data to the disk. This is useful for transient data objects. + +- Added the classmethod :meth:`.Coverage.current` to get the latest started + Coverage instance. + +- Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes + `issue 828`_. + +- Error handling during reporting has changed slightly. All reporting methods + now behave the same. The ``--ignore-errors`` option keeps errors from + stopping the reporting, but files that couldn't parse as Python will always + be reported as warnings. As with other warnings, you can suppress them with + the ``[run] disable_warnings`` configuration setting. + +- Coverage.py no longer fails if the user program deletes its current + directory. Fixes `issue 806`_. Thanks, Dan Hemberger. + +- The scrollbar markers in the HTML report now accurately show the highlighted + lines, regardless of what categories of line are highlighted. + +- The hack to accommodate ShiningPanda_ looking for an obsolete internal data + file has been removed, since ShiningPanda 0.22 fixed it four years ago. + +- The deprecated `Reporter.file_reporters` property has been removed. + +.. _ShiningPanda: https://wiki.jenkins.io/display/JENKINS/ShiningPanda+Plugin +.. _issue 806: https://github.com/nedbat/coveragepy/pull/806 +.. _issue 828: https://github.com/nedbat/coveragepy/issues/828 + + +.. _changes_50a5: + +Version 5.0a5 --- 2019-05-07 +---------------------------- + +- Drop support for Python 3.4 + +- Dynamic contexts can now be set two new ways, both thanks to Justas + Sadzevičius. + + - A plugin can implement a ``dynamic_context`` method to check frames for + whether a new context should be started. See + :ref:`dynamic_context_plugins` for more details. + + - Another tool (such as a test runner) can use the new + :meth:`.Coverage.switch_context` method to explicitly change the context. + +- The ``dynamic_context = test_function`` setting now works with Python 2 + old-style classes, though it only reports the method name, not the class it + was defined on. Closes `issue 797`_. + +- ``fail_under`` values more than 100 are reported as errors. Thanks to Mike + Fiedler for closing `issue 746`_. + +- The "missing" values in the text output are now sorted by line number, so + that missing branches are reported near the other lines they affect. The + values used to show all missing lines, and then all missing branches. + +- Access to the SQLite database used for data storage is now thread-safe. + Thanks, Stephan Richter. This closes `issue 702`_. + +- Combining data stored in SQLite is now about twice as fast, fixing `issue + 761`_. Thanks, Stephan Richter. + +- The ``filename`` attribute on :class:`.CoverageData` objects has been made + private. You can use the ``data_filename`` method to get the actual file + name being used to store data, and the ``base_filename`` method to get the + original filename before parallelizing suffixes were added. This is part of + fixing `issue 708`_. + +- Line numbers in the HTML report now align properly with source lines, even + when Chrome's minimum font size is set, fixing `issue 748`_. Thanks Wen Ye. + +.. _issue 702: https://github.com/nedbat/coveragepy/issues/702 +.. _issue 708: https://github.com/nedbat/coveragepy/issues/708 +.. _issue 746: https://github.com/nedbat/coveragepy/issues/746 +.. _issue 748: https://github.com/nedbat/coveragepy/issues/748 +.. _issue 761: https://github.com/nedbat/coveragepy/issues/761 +.. _issue 797: https://github.com/nedbat/coveragepy/issues/797 + + +.. _changes_50a4: + +Version 5.0a4 --- 2018-11-25 +---------------------------- + +- You can specify the command line to run your program with the ``[run] + command_line`` configuration setting, as requested in `issue 695`_. + +- Coverage will create directories as needed for the data file if they don't + exist, closing `issue 721`_. + +- The ``coverage run`` command has always adjusted the first entry in sys.path, + to properly emulate how Python runs your program. Now this adjustment is + skipped if sys.path[0] is already different than Python's default. This + fixes `issue 715`_. + +- Improvements to context support: + + - The "no such table: meta" error is fixed.: `issue 716`_. + + - Combining data files is now much faster. + +- Python 3.8 (as of today!) passes all tests. + +.. _issue 695: https://github.com/nedbat/coveragepy/issues/695 +.. _issue 715: https://github.com/nedbat/coveragepy/issues/715 +.. _issue 716: https://github.com/nedbat/coveragepy/issues/716 +.. _issue 721: https://github.com/nedbat/coveragepy/issues/721 + + +.. _changes_50a3: + +Version 5.0a3 --- 2018-10-06 +---------------------------- + +- Context support: static contexts let you specify a label for a coverage run, + which is recorded in the data, and retained when you combine files. See + :ref:`contexts` for more information. + +- Dynamic contexts: specifying ``[run] dynamic_context = test_function`` in the + config file will record the test function name as a dynamic context during + execution. This is the core of "Who Tests What" (`issue 170`_). Things to + note: + + - There is no reporting support yet. Use SQLite to query the .coverage file + for information. Ideas are welcome about how reporting could be extended + to use this data. + + - There's a noticeable slow-down before any test is run. + + - Data files will now be roughly N times larger, where N is the number of + tests you have. Combining data files is therefore also N times slower. + + - No other values for ``dynamic_context`` are recognized yet. Let me know + what else would be useful. I'd like to use a pytest plugin to get better + information directly from pytest, for example. + +.. _issue 170: https://github.com/nedbat/coveragepy/issues/170 + +- Environment variable substitution in configuration files now supports two + syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` + is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default + value}`` will use "default value". + +- Partial support for Python 3.8, which has not yet released an alpha. Fixes + `issue 707`_ and `issue 714`_. + +.. _issue 707: https://github.com/nedbat/coveragepy/issues/707 +.. _issue 714: https://github.com/nedbat/coveragepy/issues/714 + + +.. _changes_50a2: + +Version 5.0a2 --- 2018-09-03 +---------------------------- + +- Coverage's data storage has changed. In version 4.x, .coverage files were + basically JSON. Now, they are SQLite databases. This means the data file + can be created earlier than it used to. A large amount of code was + refactored to support this change. + + - Because the data file is created differently than previous releases, you + may need ``parallel=true`` where you didn't before. + + - The old data format is still available (for now) by setting the environment + variable COVERAGE_STORAGE=json. Please tell me if you think you need to + keep the JSON format. + + - The database schema is guaranteed to change in the future, to support new + features. I'm looking for opinions about making the schema part of the + public API to coverage.py or not. + +- Development moved from `Bitbucket`_ to `GitHub`_. + +- HTML files no longer have trailing and extra whitespace. + +- The sort order in the HTML report is stored in local storage rather than + cookies, closing `issue 611`_. Thanks, Federico Bond. + +- pickle2json, for converting v3 data files to v4 data files, has been removed. + +.. _Bitbucket: https://bitbucket.org/ned/coveragepy +.. _GitHub: https://github.com/nedbat/coveragepy + +.. _issue 611: https://github.com/nedbat/coveragepy/issues/611 + + +.. _changes_50a1: + +Version 5.0a1 --- 2018-06-05 +---------------------------- + +- Coverage.py no longer supports Python 2.6 or 3.3. + +- The location of the configuration file can now be specified with a + ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_. + +- Namespace packages are supported on Python 3.7, where they used to cause + TypeErrors about path being None. Fixes `issue 700`_. + +- A new warning (``already-imported``) is issued if measurable files have + already been imported before coverage.py started measurement. See + :ref:`cmd_warnings` for more information. + +- Running coverage many times for small runs in a single process should be + faster, closing `issue 625`_. Thanks, David MacIver. + +- Large HTML report pages load faster. Thanks, Pankaj Pandey. + +.. _issue 625: https://bitbucket.org/ned/coveragepy/issues/625/lstat-dominates-in-the-case-of-small +.. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location +.. _issue 700: https://github.com/nedbat/coveragepy/issues/700 + + +.. _changes_454: + +Version 4.5.4 --- 2019-07-29 +---------------------------- + +- Multiprocessing support in Python 3.8 was broken, but is now fixed. Closes + `issue 828`_. + +.. _issue 828: https://github.com/nedbat/coveragepy/issues/828 + + +.. _changes_453: + +Version 4.5.3 --- 2019-03-09 +---------------------------- + +- Only packaging metadata changes. + + +.. _changes_452: + +Version 4.5.2 --- 2018-11-12 +---------------------------- + +- Namespace packages are supported on Python 3.7, where they used to cause + TypeErrors about path being None. Fixes `issue 700`_. + +- Python 3.8 (as of today!) passes all tests. Fixes `issue 707`_ and + `issue 714`_. + +- Development moved from `Bitbucket`_ to `GitHub`_. + +.. _issue 700: https://github.com/nedbat/coveragepy/issues/700 +.. _issue 707: https://github.com/nedbat/coveragepy/issues/707 +.. _issue 714: https://github.com/nedbat/coveragepy/issues/714 + +.. _Bitbucket: https://bitbucket.org/ned/coveragepy +.. _GitHub: https://github.com/nedbat/coveragepy + + .. _changes_451: Version 4.5.1 --- 2018-02-10 @@ -41,7 +670,7 @@ Version 4.5.1 --- 2018-02-10 Version 4.5 --- 2018-02-03 -------------------------- -- A new kind of plugin is supported: configurators are invoked at start-up to +- A new kind of plugin is supported: configurers are invoked at start-up to allow more complex configuration than the .coveragerc file can easily do. See :ref:`api_plugin` for details. This solves the complex configuration problem described in `issue 563`_. @@ -258,7 +887,7 @@ Version 4.3.2 --- 2017-01-16 .. _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 522: https://bitbucket.org/ned/coveragepy/issues/522/incorrect-branch-reporting .. _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 @@ -485,7 +1114,7 @@ Work from the PyCon 2016 Sprints! .. _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 -.. _unittest-mixins: https://pypi.python.org/pypi/unittest-mixins +.. _unittest-mixins: https://pypi.org/project/unittest-mixins/ .. _changes_41: @@ -572,9 +1201,9 @@ Version 4.1b2 --- 2016-01-23 - Class docstrings were considered executable. Now they no longer are. - ``yield from`` and ``await`` were considered returns from functions, since - they could tranfer control to the caller. This produced unhelpful "missing - branch" reports in a number of circumstances. Now they no longer are - considered returns. + they could transfer control to the caller. This produced unhelpful + "missing branch" reports in a number of circumstances. Now they no longer + are considered returns. - In unusual situations, a missing branch to a negative number was reported. This has been fixed, closing `issue 466`_. @@ -655,7 +1284,7 @@ Version 4.0.3 --- 2015-11-24 - The XML report now has correct ```` elements when using a ``--source=`` option somewhere besides the current directory. This fixes - `issue 439`_. Thanks, Arcady Ivanov. + `issue 439`_. Thanks, Arcadiy Ivanov. - Fixed an unusual edge case of detecting source encodings, described in `issue 443`_. @@ -754,7 +1383,7 @@ Version 4.0b3 --- 2015-09-07 - Reporting on an unmeasured file would fail with a traceback. This is now fixed, closing `issue 403`_. -- The Jenkins ShiningPanda plugin looks for an obsolete file name to find the +- The Jenkins ShiningPanda_ plugin looks for an obsolete file name to find the HTML reports to publish, so it was failing under coverage.py 4.0. Now we create that file if we are running under Jenkins, to keep things working smoothly. `issue 404`_. @@ -1804,7 +2433,7 @@ Version 3.2b4 --- 2009-12-01 - On Python 3.x, setuptools has been replaced by `Distribute`_. -.. _Distribute: https://pypi.python.org/pypi/distribute +.. _Distribute: https://pypi.org/project/distribute/ Version 3.2b3 --- 2009-11-23 @@ -2017,7 +2646,7 @@ Major overhaul. - The singleton coverage object is only created if the module-level functions are used. This maintains the old interface while allowing better - programmatic use of Coverage.py. + programmatic use of coverage.py. - The minimum supported Python version is 2.3. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 52cedc0f5..99b8493f4 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -4,20 +4,27 @@ extended and maintained by Ned Batchelder. Other contributions, including writing code, updating docs, and submitting useful bug reports, have been made by: +Abdeali Kothari Adi Roiban +Agbonze O. Jeremiah +Albertas Agejevas +Aleksi Torhamo Alex Gaynor Alex Groce Alex Sandro Alexander Todorov +Alexander Walters Andrew Hoos Anthony Sottile Arcadiy Ivanov Aron Griffis Artem Dayneko Ben Finney +Bernát Gábor Bill Hart Brandon Rhodes Brett Cannon +Bruno P. Kinoshita Buck Evan Calen Pennington Carl Gieringer @@ -31,6 +38,7 @@ Christine Lytwynec Christoph Zwerschke Conrad Ho Cosimo Lupo +Dan Hemberger Dan Riti Dan Wandschneider Danek Duvall @@ -39,30 +47,42 @@ Danny Allen David Christian David MacIver David Stanek +David Szotten Detlev Offenbach Devin Jeanpierre Dirk Thomas Dmitry Shishov Dmitry Trofimov Eduardo Schettino +Eli Skeggs Emil Madsen Edward Loper +Federico Bond +Frazer McLean Geoff Bache George Paci George Song +George-Cristian Bîrzan Greg Rogers +Guido van Rossum Guillaume Chazarain +Hugo van Kemenade Ilia Meerovich Imri Goldberg Ionel Cristian Mărieș JT Olds +Jerin Peter George Jessamyn Smith Joe Doherty +Joe Jevnik Jon Chappell Jon Dufresne Joseph Tate Josh Williams Julian Berman +Julien Voisin +Justas Sadzevičius +Kjell Braden Krystian Kichewko Kyle Altendorf Lars Hupfeldt Nielsen @@ -71,35 +91,51 @@ Lex Berezhny Loïc Dachary Marc Abramowitz Marcus Cobden +Marius Gedminas Mark van der Wal Martin Fuzzey +Matt Bachmann Matthew Boehm Matthew Desmarais Max Linke +Michał Bultrowicz Mickie Betz +Mike Fiedler +Naveen Yadav Nathan Land Noel O'Boyle Olivier Grisel +Ori Avtalion +Pankaj Pandey Pablo Carballo Patrick Mezard Peter Baughman Peter Ebden Peter Portante +Reya B Rodrigue Cloutier Roger Hu Ross Lawley Roy Williams +Salvatore Zagaria Sandra Martocchia Scott Belden Sigve Tjora +Simon Willison Stan Hu Stefan Behnel +Stephan Richter Stephen Finucane Steve Leonard Steve Peak +S. Y. Lee +Teake Nutma Ted Wexler +Thijs Triemstra Titus Brown +Vince Salvino Ville Skyttä +Xie Yanbo Yury Selivanov Zac Hatfield-Dodds Zooko Wilcox-O'Hearn diff --git a/MANIFEST.in b/MANIFEST.in index 462f24ff7..75257c606 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,13 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # MANIFEST.in file for coverage.py +# This file includes everything needed to recreate the entire project, even +# though many of these files are not installed by setup.py. Unpacking the +# .tar.gz source distribution would give you everything needed to continue +# developing the project. "pip install" will not install many of these files. + include CONTRIBUTORS.txt include CHANGES.rst include LICENSE.txt @@ -10,11 +15,9 @@ include MANIFEST.in include Makefile include NOTICE.txt include README.rst -include TODO.txt include __main__.py include .travis.yml include appveyor.yml -include circle.yml include howto.txt include igor.py include metacov.ini @@ -22,25 +25,25 @@ include pylintrc include setup.py include tox.ini include tox_wheels.ini +include .editorconfig +include .readthedocs.yml -recursive-include ci *.* -exclude ci/appveyor.token +recursive-include ci * +exclude ci/*.token recursive-include coverage/fullcoverage *.py recursive-include coverage/ctracer *.c *.h -recursive-include doc conf.py *.pip *.rst *.txt -recursive-include doc/_static *.* +recursive-include doc *.py *.pip *.rst *.txt *.png +recursive-include doc/_static * prune doc/_build +prune doc/_spell recursive-include requirements *.pip recursive-include tests *.py *.tok -recursive-include tests/farm */gold*/*.* */gold*/*/*.* -recursive-include tests/farm/*/src * *.* -recursive-include tests js/*.* qunit/*.* +recursive-include tests/gold * +recursive-include tests js/* qunit/* prune tests/eggsrc/build -prune tests/eggsrc/dist -prune tests/eggsrc/*.egg-info global-exclude *.py[co] diff --git a/Makefile b/Makefile index 21e8856b8..0c65bb526 100644 --- a/Makefile +++ b/Makefile @@ -1,83 +1,113 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Makefile for utility work on coverage.py. -default: - @echo "* No default action *" +help: ## Show this help. + @echo "Available targets:" + @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf " %-26s%s\n", $$1, $$2}' -clean: +clean_platform: ## Remove files that clash across platforms. + rm -f *.so */*.so + rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ + rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc */*/*/*/*.pyc */*/*/*/*/*.pyc + rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo */*/*/*/*.pyo */*/*/*/*/*.pyo + +clean: clean_platform ## Remove artifacts of test execution, installation, etc. -pip uninstall -y coverage - -rm -f *.pyd */*.pyd - -rm -f *.so */*.so - -PYTHONPATH=. python tests/test_farm.py clean - -rm -rf tests/farm/*/out - -rm -rf build coverage.egg-info dist htmlcov - -rm -f *.pyc */*.pyc */*/*.pyc */*/*/*.pyc */*/*/*/*.pyc */*/*/*/*/*.pyc - -rm -f *.pyo */*.pyo */*/*.pyo */*/*/*.pyo */*/*/*/*.pyo */*/*/*/*/*.pyo - -rm -f *.bak */*.bak */*/*.bak */*/*/*.bak */*/*/*/*.bak */*/*/*/*/*.bak - -rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class - -rm -rf __pycache__ */__pycache__ */*/__pycache__ */*/*/__pycache__ */*/*/*/__pycache__ */*/*/*/*/__pycache__ - -rm -f coverage/*,cover - -rm -f MANIFEST - -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 doc/sample_html_beta - -rm -rf .tox_kits - -sterile: clean - -rm -rf .tox* + rm -f *.pyd */*.pyd + rm -rf build coverage.egg-info dist htmlcov + rm -f *.bak */*.bak */*/*.bak */*/*/*.bak */*/*/*/*.bak */*/*/*/*/*.bak + rm -f *$$py.class */*$$py.class */*/*$$py.class */*/*/*$$py.class */*/*/*/*$$py.class */*/*/*/*/*$$py.class + rm -f coverage/*,cover + rm -f MANIFEST + rm -f .coverage .coverage.* coverage.xml .metacov* + rm -f .tox/*/lib/*/site-packages/zzz_metacov.pth + rm -f */.coverage */*/.coverage */*/*/.coverage */*/*/*/.coverage */*/*/*/*/.coverage */*/*/*/*/*/.coverage + rm -f tests/covmain.zip 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 doc/sample_html_beta + rm -rf tmp + rm -rf .cache .pytest_cache .hypothesis + rm -rf $$TMPDIR/coverage_test + -make -C tests/gold/html clean + +sterile: clean ## Remove all non-controlled content, even if expensive. + rm -rf .tox + -docker image rm -f quay.io/pypa/manylinux1_i686 quay.io/pypa/manylinux1_x86_64 + + +CSS = coverage/htmlfiles/style.css +SCSS = coverage/htmlfiles/style.scss + +css: $(CSS) ## Compile .scss into .css. +$(CSS): $(SCSS) + pysassc --style=compact $(SCSS) $@ + cp $@ tests/gold/html/styled LINTABLE = coverage tests igor.py setup.py __main__.py -lint: +lint: ## Run linters and checkers. tox -e lint todo: -grep -R --include=*.py TODO $(LINTABLE) -spell: - -pylint --disable=all --enable=spelling $(LINTABLE) - pep8: pycodestyle --filename=*.py --repeat $(LINTABLE) test: tox -e py27,py35 $(ARGS) -TOX_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) +PYTEST_SMOKE_ARGS = -n 6 -m "not expensive" --maxfail=3 $(ARGS) + +smoke: ## Run tests quickly with the C tracer in the lowest supported Python versions. + COVERAGE_NO_PYTRACER=1 tox -q -e py27,py35 -- $(PYTEST_SMOKE_ARGS) + +pysmoke: ## Run tests quickly with the Python tracer in the lowest supported Python versions. + COVERAGE_NO_CTRACER=1 tox -q -e py27,py35 -- $(PYTEST_SMOKE_ARGS) -smoke: - COVERAGE_NO_PYTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS) +DOCKER_RUN = docker run -it --init --rm -v `pwd`:/io +RUN_MANYLINUX_X86 = $(DOCKER_RUN) quay.io/pypa/manylinux1_x86_64 /io/ci/manylinux.sh +RUN_MANYLINUX_I686 = $(DOCKER_RUN) quay.io/pypa/manylinux1_i686 /io/ci/manylinux.sh -pysmoke: - COVERAGE_NO_CTRACER=1 tox -e py26,py33 -- $(TOX_SMOKE_ARGS) +test_linux: ## Run the tests in Linux under Docker. + # The Linux .pyc files clash with the host's because of file path + # changes, so clean them before and after running tests. + make clean_platform + $(RUN_MANYLINUX_X86) test $(ARGS) + make clean_platform -metacov: +meta_linux: ## Run meta-coverage in Linux under Docker. + ARGS="meta $(ARGS)" make test_linux + +# Coverage measurement of coverage.py itself (meta-coverage). See metacov.ini +# for details. + +metacov: ## Run meta-coverage, measuring ourself. COVERAGE_COVERAGE=yes tox $(ARGS) -metahtml: +metahtml: ## Produce meta-coverage HTML reports. python igor.py combine_html # Kitting -kit: - python setup.py sdist --formats=gztar +kit: ## Make the source distribution. + python setup.py sdist -wheel: +wheel: ## Make the wheels for distribution. tox -c tox_wheels.ini $(ARGS) -manylinux_clean: - docker image rm quay.io/pypa/manylinux1_i686 quay.io/pypa/manylinux1_x86_64 +kit_linux: ## Make the Linux wheels. + $(RUN_MANYLINUX_X86) build + $(RUN_MANYLINUX_I686) build -manylinux: - docker run -it --init --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/ci/manylinux.sh build - docker run -it --init --rm -v `pwd`:/io quay.io/pypa/manylinux1_i686 /io/ci/manylinux.sh build +kit_upload: ## Upload the built distributions to PyPI. + twine upload --verbose dist/* -kit_upload: - twine upload dist/* +test_upload: ## Upload the distrubutions to PyPI's testing server. + twine upload --verbose --repository testpypi dist/* kit_local: # pip.conf looks like this: @@ -88,37 +118,49 @@ kit_local: # don't go crazy trying to figure out why our new code isn't installing. find ~/Library/Caches/pip/wheels -name 'coverage-*' -delete -download_appveyor: +download_appveyor: ## Download the latest Windows artifacts from AppVeyor. python ci/download_appveyor.py nedbat/coveragepy build_ext: python setup.py build_ext -install: - python setup.py install - -uninstall: - -rm -rf $(PYHOME)/lib/site-packages/coverage* - -rm -rf $(PYHOME)/scripts/coverage* - # Documentation -SPHINXBUILD = sphinx-build -SPHINXOPTS = -a -E doc +DOCBIN = .tox/doc/bin +SPHINXOPTS = -aE +SPHINXBUILD = $(DOCBIN)/sphinx-build $(SPHINXOPTS) +SPHINXAUTOBUILD = $(DOCBIN)/sphinx-autobuild -p 9876 --ignore '.git/**' --open-browser WEBHOME = ~/web/stellated/ WEBSAMPLE = $(WEBHOME)/files/sample_coverage_html WEBSAMPLEBETA = $(WEBHOME)/files/sample_coverage_html_beta -docreqs: - pip install -r doc/requirements.pip - -dochtml: - PYTHONPATH=$(CURDIR) $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html - @echo - @echo "Build finished. The HTML pages are in doc/_build/html." - -docspell: - $(SPHINXBUILD) -b spelling $(SPHINXOPTS) doc/_spell +$(DOCBIN): + tox -q -e doc --notest + +cmd_help: $(DOCBIN) + mkdir -p tmp + for cmd in annotate combine debug erase html json report run xml; do \ + echo > tmp/$$cmd.rst; \ + echo ".. code::" >> tmp/$$cmd.rst; \ + echo >> tmp/$$cmd.rst; \ + echo " $$ coverage $$cmd --help" >> tmp/$$cmd.rst; \ + $(DOCBIN)/python -m coverage $$cmd --help | \ + sed \ + -e 's/__main__.py/coverage/' \ + -e '/^Full doc/d' \ + -e 's/^/ /' \ + >> tmp/$$cmd.rst; \ + done + +dochtml: $(DOCBIN) cmd_help ## Build the docs HTML output. + $(DOCBIN)/python doc/check_copied_from.py doc/*.rst + $(SPHINXBUILD) -b html doc doc/_build/html + +docdev: dochtml ## Build docs, and auto-watch for changes. + PATH=$(DOCBIN):$(PATH) $(SPHINXAUTOBUILD) -b html doc doc/_build/html + +docspell: $(DOCBIN) ## Run the spell checker on the docs. + $(SPHINXBUILD) -b spelling doc doc/_spell publish: rm -f $(WEBSAMPLE)/*.* @@ -129,3 +171,20 @@ publishbeta: rm -f $(WEBSAMPLEBETA)/*.* mkdir -p $(WEBSAMPLEBETA) cp doc/sample_html_beta/*.* $(WEBSAMPLEBETA) + +CHANGES_MD = tmp/rst_rst/changes.md +RELNOTES_JSON = tmp/relnotes.json + +$(CHANGES_MD): CHANGES.rst $(DOCBIN) + $(SPHINXBUILD) -b rst doc tmp/rst_rst + pandoc -frst -tmarkdown_strict --atx-headers --wrap=none tmp/rst_rst/changes.rst > $(CHANGES_MD) + +relnotes_json: $(RELNOTES_JSON) ## Convert changelog to JSON for further parsing. +$(RELNOTES_JSON): $(CHANGES_MD) + $(DOCBIN)/python ci/parse_relnotes.py tmp/rst_rst/changes.md $(RELNOTES_JSON) + +tidelift_relnotes: $(RELNOTES_JSON) ## Upload parsed release notes to Tidelift. + $(DOCBIN)/python ci/tidelift_relnotes.py $(RELNOTES_JSON) pypi/coverage + +github_releases: $(RELNOTES_JSON) ## Update GitHub releases. + $(DOCBIN)/python ci/github_releases.py $(RELNOTES_JSON) nedbat/coveragepy diff --git a/NOTICE.txt b/NOTICE.txt index e5a99515a..2e7671024 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Copyright 2001 Gareth Rees. All rights reserved. -Copyright 2004-2018 Ned Batchelder. All rights reserved. +Copyright 2004-2020 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 diff --git a/README.rst b/README.rst index 4dac3ea85..778b45a5a 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt =========== Coverage.py @@ -7,11 +7,11 @@ Coverage.py Code coverage testing for Python. -| |license| |versions| |status| |docs| -| |ci-status| |win-ci-status| |codecov| -| |kit| |format| |saythanks| - -.. downloads badge seems to be broken... |downloads| +| |license| |versions| |status| +| |ci-status| |win-ci-status| |docs| |codecov| +| |kit| |format| |repos| |downloads| +| |stars| |forks| |contributors| +| |tidelift| |twitter-coveragepy| |twitter-nedbat| Coverage.py measures code coverage, typically during test execution. It uses the code analysis tools and tracing hooks provided in the Python standard @@ -19,32 +19,38 @@ library to determine which lines are executable, and which have been executed. Coverage.py runs on many versions of Python: -* CPython 2.6, 2.7 and 3.3 through 3.7. -* PyPy2 5.10 and PyPy3 5.10. -* Jython 2.7.1, though not for reporting. -* IronPython 2.7.7, though not for reporting. +* CPython 2.7. +* CPython 3.5 through 3.9 beta 4. +* PyPy2 7.3.1 and PyPy3 7.3.1. Documentation is on `Read the Docs`_. Code repository and issue tracker are on -`Bitbucket`_, with a mirrored repository on `GitHub`_. +`GitHub`_. .. _Read the Docs: https://coverage.readthedocs.io/ -.. _Bitbucket: https://bitbucket.org/ned/coveragepy .. _GitHub: https://github.com/nedbat/coveragepy -**New in 4.5:** Configurator plug-ins. +**New in 5.x:** SQLite data storage, JSON report, contexts, relative filenames, +dropped support for Python 2.6, 3.3 and 3.4. -New in 4.4: Suppressable warnings, continuous coverage measurement. -New in 4.3: HTML ``--skip-covered``, sys.excepthook support, tox.ini -support. +For Enterprise +-------------- -New in 4.2: better support for multiprocessing and combining data. +.. |tideliftlogo| image:: https://nedbatchelder.com/pix/Tidelift_Logo_small.png + :alt: Tidelift + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme -New in 4.1: much-improved branch coverage. +.. list-table:: + :widths: 10 100 -New in 4.0: ``--concurrency``, plugins for non-Python files, setup.cfg -support, --skip-covered, HTML filtering, and more than 50 issues closed. + * - |tideliftlogo| + - `Available as part of the Tidelift Subscription. `_ + Coverage and thousands of other packages are working with + Tidelift to deliver one enterprise subscription that covers all of the open + source you use. If you want the flexibility of open source and the confidence + of commercial-grade software, this is for you. + `Learn more. `_ Getting Started @@ -55,6 +61,14 @@ See the `Quick Start section`_ of the docs. .. _Quick Start section: https://coverage.readthedocs.io/#quick-start +Change history +-------------- + +The complete history of changes is on the `change history page`_. + +.. _change history page: https://coverage.readthedocs.io/en/latest/changes.html + + Contributing ------------ @@ -63,17 +77,26 @@ See the `Contributing section`_ of the docs. .. _Contributing section: https://coverage.readthedocs.io/en/latest/contributing.html +Security +-------- + +To report a security vulnerability, please use the `Tidelift security +contact`_. Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security + + License ------- 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 +.. _NOTICE.txt: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -.. |ci-status| image:: https://travis-ci.org/nedbat/coveragepy.svg?branch=master - :target: https://travis-ci.org/nedbat/coveragepy +.. |ci-status| image:: https://travis-ci.com/nedbat/coveragepy.svg?branch=master + :target: https://travis-ci.com/nedbat/coveragepy :alt: Build status .. |win-ci-status| image:: https://ci.appveyor.com/api/projects/status/kmeqpdje7h9r6vsf/branch/master?svg=true :target: https://ci.appveyor.com/project/nedbat/coveragepy @@ -85,26 +108,44 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. :target: https://requires.io/github/nedbat/coveragepy/requirements/?branch=master :alt: Requirements status .. |kit| image:: https://badge.fury.io/py/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: PyPI status .. |format| image:: https://img.shields.io/pypi/format/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Kit format .. |downloads| image:: https://img.shields.io/pypi/dw/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Weekly PyPI downloads -.. |versions| image:: https://img.shields.io/pypi/pyversions/coverage.svg - :target: https://pypi.python.org/pypi/coverage +.. |versions| image:: https://img.shields.io/pypi/pyversions/coverage.svg?logo=python&logoColor=FBE072 + :target: https://pypi.org/project/coverage/ :alt: Python versions supported .. |status| image:: https://img.shields.io/pypi/status/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: Package stability .. |license| image:: https://img.shields.io/pypi/l/coverage.svg - :target: https://pypi.python.org/pypi/coverage + :target: https://pypi.org/project/coverage/ :alt: License -.. |codecov| image:: http://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 - :target: http://codecov.io/github/nedbat/coveragepy?branch=master +.. |codecov| image:: https://codecov.io/github/nedbat/coveragepy/coverage.svg?branch=master&precision=2 + :target: https://codecov.io/github/nedbat/coveragepy?branch=master :alt: Coverage! -.. |saythanks| image:: https://img.shields.io/badge/saythanks.io-%E2%98%BC-1EAEDB.svg - :target: https://saythanks.io/to/nedbat - :alt: Say thanks :) +.. |repos| image:: https://repology.org/badge/tiny-repos/python:coverage.svg + :target: https://repology.org/metapackage/python:coverage/versions + :alt: Packaging status +.. |tidelift| image:: https://tidelift.com/badges/package/pypi/coverage + :target: https://tidelift.com/subscription/pkg/pypi-coverage?utm_source=pypi-coverage&utm_medium=referral&utm_campaign=readme + :alt: Tidelift +.. |stars| image:: https://img.shields.io/github/stars/nedbat/coveragepy.svg?logo=github + :target: https://github.com/nedbat/coveragepy/stargazers + :alt: Github stars +.. |forks| image:: https://img.shields.io/github/forks/nedbat/coveragepy.svg?logo=github + :target: https://github.com/nedbat/coveragepy/network/members + :alt: Github forks +.. |contributors| image:: https://img.shields.io/github/contributors/nedbat/coveragepy.svg?logo=github + :target: https://github.com/nedbat/coveragepy/graphs/contributors + :alt: Contributors +.. |twitter-coveragepy| image:: https://img.shields.io/twitter/follow/coveragepy.svg?label=coveragepy&style=flat&logo=twitter&logoColor=4FADFF + :target: https://twitter.com/coveragepy + :alt: coverage.py on Twitter +.. |twitter-nedbat| image:: https://img.shields.io/twitter/follow/nedbat.svg?label=nedbat&style=flat&logo=twitter&logoColor=4FADFF + :target: https://twitter.com/nedbat + :alt: nedbat on Twitter diff --git a/TODO.txt b/TODO.txt deleted file mode 100644 index f6036d2e5..000000000 --- a/TODO.txt +++ /dev/null @@ -1,285 +0,0 @@ -Coverage.py TODO - -Key: - * Heading - - Not done yet. - + Done. - x Not going to do. - -* 4.0 - -- What defaults should change? - x --source = . ? - x --branch = True ? - -- Remove 2.3, 2.4, 2.5 limitations - + set, sorted, reversed, rpartition - + generator expressions - + decorators - + collections.defaultdict - + .startswith((,)) - + "with" statements - - .format() ? - + try/except/finally - + with assertRaises - + addCleanup instead of tearDown - + exec statement can look like a function in py2 (since when?) - - runpy ? - + we can use "except ExcClass as e:" - -- Plugins - + Clean up - + implement plugin support in CTracer - + remove plugin support from PyTracer - x add services: - - filelocator - - warning - - dynamic_source_filename: return should be a canonical path - - update the omit test to use "quux*" instead of "*quux*" - + docs -+ Make reports use filenames, not module names -- documentation - - test helpers - + cov.config["run:branch"] api (well, coverage.get_option etc) - + "added in 4.0" - - tweaks to theme? - - Plugins! - Once per process - Once per file - - create a file tracer - - call its has_dynamic_source_file() - Once per call - Once per line -- build process - - don't publish to nedbat.com any more (but still need the sample html reports) - + don't need .px tooling - - write a new nedbat.com/code/coverage page. - - all doc links should point to rtfd -+ Remove code only run on <2.6 -+ Change data file to json -+ Create data api -+ gevent, etc. -+ Remove the old command-line syntax - + A pain, b/c of the structure of the tests. - + BTW: make an easier way to write those tests. - -- tests - - test the kit has the right contents - - test the kit installs the right stuff - - -* --source stuff: - + warn if a package is never found. - + warn if no data was collected - - tie --source into reporting - -* Soon - -+ Better omit handling that ignores files during measurement. - - Deal with ~ in specified paths correctly. -+ while TRUE claims to be partial. - + A way to mark lines as partial branches, with a regex? - + Default to "while True:", "while 1:" -+ HTML keyboard short cuts - - -* 3.2 - -+ Some kind of indication in the HTML where yellow lines aren't going. -- Profile the reporting code: it's REALLY slow. - - parser is doing some redundant work. -+ Analysis class should do rolling up of stats also (actually Numbers) -+ Update docs for --branch. -x self.coverage.data.has_arcs is ugly. -+ Branches that never jump to nocover lines shouldn't be marked as partial. - (see top of test_cogapp for examples) -+ Maybe turning off yellow lines should make those lines green? -+ A missing branch to leave the function shows an annotation of -1. Now "exit". -+ XML report needs to get branch information. -+ Add branch info to "coverage debug data" -+ Polish up the help, and double-check the docs. - - -* Speed - -+ C extension collector -- bitvector in trace extension. -- Ignore certain modules -+ Record linenos rather than (file,lineno) pairs in tracer. -x Tricky swapping of collector like figleaf, pycov, et al. (Don't need to do - this with C collector). -- Seems like there should be a faster way to manage all the line number sets in - CodeParser.raw_parse. -- If tracing, canonical_filename_cache overlaps with should_trace_cache. Skip - canonical_filename_cache. Maybe it isn't even worth it... -- Would pre-allocating line number integers make the C tracer faster? It would - use less memory. - - -* Accuracy - -- Record magic number of module to ensure code hasn't changed -- Record version of coverage data file, so we can update what's stored there. -- Record options in coverage data file, so multiple runs are certain to make - sense together. -- Do I still need the lines in annotate_file that deal specially with "else"? - - -* Power - -+ Branch coverage - Titus' idea: - 1: if a: - 2: b = 2 - 3: c = 3 - if the coverage data shows 1,2,3, it was if-then. if it's 1,3, then the - missing else was executed. -+ API for getting coverage data. -- Instruction tracing instead of line tracing. -- Path tracing (how does this even work?) -- Count execution of lines -- Track callers of functions (ala std module trace) -- Method/Class/Module coverage reporting. -- .coverage files that can be kept separate, rather than accumulated. -- test/coverage map: http://rbtcollins.wordpress.com/2009/09/16/back-from-hiatus/ - - Similar to figleaf's sections. - - -* Convenience - -- Command line modules should also be directories, meaning all the modules in that - directory. -- Why can't a morf also be a string, the name of a module? -- ignore by module as well as file? -+ Use a .coveragerc file to control coverage.py without the programmatic API. -- Add a --data switch to explicitly control the data file on the command line. -x Why can't you specify execute (-x) and report (-r) in the same invocation? - Maybe just because -x needs the rest of the command line? -+ Support 2.3 - 3.1! - http://pythonology.blogspot.com/2009/02/making-code-run-on-python-20-through-30.html - http://www.rfk.id.au/blog/entry/preparing-pyenchant-for-python-3 - http://pydev.blogspot.com/2008/11/making-code-work-in-python-2-and-3.html - + Explicitly set pickle protocol to 2. -- An inference mode that marks lines as executed if they "must have been" executed: - class definitions, etc, when coverage is started after the class is defined. -- Different categories of exclude pragma? So you can enable and disable them - from the command line, to reconsider exclusions. -+ Reporting on files never touched by coverage.py (package completeness) -- A setup.py command? http://jeetworks.org/node/50 -- Deltas: indicate the change in coverage percentage from the last run. -+ Show lines missing rather than lines run in the reporting, since that's what - you need to focus on. - - -* Beauty - -+ HTML report - - Colored bars indicating coverage per file. - - Package navigation. - - Rolled-up statistics. - - Some way to focus in on red and yellow - - Show only lines near highlights? - + Jump to next highlight? - + Keyboard navigation: j and k. - - Cookie for changes to pyfile.html state. - + Clickable column headers on the index page. - + Syntax coloring in HTML report. - + Dynamic effects in HTML report. - + Footer in reports pointing to coverage.py home page. - + Baseline grid for linenumber font. - + Separate out css and HTML. - + Does it work right with utf-8 source files? http://www.python.org/dev/peps/pep-0263/ - - Use vim modeline to determine tab width: http://vimdoc.sourceforge.net/htmldoc/options.html#modeline - - -* Community - -+ New docs, rather than pointing to Gareth's - + Min python version is 2.3. - - Three phases of work: - - Collection - - Analysis - - Reporting - - Distinction between: - - ignore (files not to collect) - - exclude (lines not to report as missed) - - omit (files not to report) - - Changes from coverage.py 2.x: - - Bare "except:" lines now count as executable code. - - Double function decorators: all decorator lines count as executable code. - x Document the .coverage file format. - + HTML reporting. - - Much more detail about what's in the report. - - References between pages are off: - - They have tags around them. - - They use #anchors that don't survive the px->html conversion. -+ Be sure --help text is complete (-i is missing). -+ Host the project somewhere with a real bug tracker: bitbucket.org -+ Point discussion to TIP -- PEP 8 compliance? - - -* Programmability - -+ Don't use sys.exit in CoverageScript. -+ Remove singleton - + Initialization of instance variables in the class. - - -* Installation - -x How will coverage.py package install over coverage.py module? -x pip can't install it: it reads the coverage.py html page, and finds the kit link, - but then can't handle the root-relative link. - - -* Modernization - -+ Decide on minimum supported version - + 2.3 - + Get rid of the basestring protection - + Use enumerate - + Use sets instead of dicts -+ Switch from getopt to optparse. -+ Get rid of the recursive nonsense. -+ Docstrings. -+ Remove huge document-style comments. -- Better names: - + self.cache -> self.cache_filename -> CoverageData.filename - + self.usecache -> CoverageData.use_file -- More classes: - - Module munging - + Coverage data files -+ Why are some imports at the top of the file, and some in functions? -+ Get rid of sys.exitfunc use. -+ True and False (with no backward adaptation: the constants are new in 2.2.1) -+ Get rid of compiler module - + In analyzing code - + In test_coverage.py -+ Style: - + lineno - + filename - - -* Correctness - -- What does -p (parallel mode) mean with -e (erase data)? - - -* Tests - -+ Switch to a real test runner, like nose. -+ Test both the C trace function and the Python trace function. -+ parser.py has no direct tests. -+ Tests about the .coverage file. -+ Tests about the --long-form of arguments. -+ Tests about overriding the .coverage filename. -- Tests about parallel mode. -+ Tests about assigning a multi-line string. -- Tests about tricky docstrings. -+ Coverage test coverage.py! -- Tests that tracing stops after calling stop() -- More intensive thread testing. -x Tests about the "import __main__" in cmdline.py -+ What happens if the -x script raises an exception? -- Test that the kit has all the proper contents. diff --git a/__main__.py b/__main__.py index c998e1da7..28ad7d2da 100644 --- a/__main__.py +++ b/__main__.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Be able to execute coverage.py by pointing Python at a working tree.""" @@ -8,12 +8,5 @@ PKG = 'coverage' -try: - run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True) - executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0] - if executed != '__main__': # For Python 2.5 compatibility - raise ImportError( - 'Incorrectly executed %s instead of __main__' % executed - ) -except ImportError: # For Python 2.6 compatibility - runpy.run_module('%s.__main__' % PKG, run_name='__main__', alter_sys=True) +run_globals = runpy.run_module(PKG, run_name='__main__', alter_sys=True) +executed = os.path.splitext(os.path.basename(run_globals['__file__']))[0] diff --git a/appveyor.yml b/appveyor.yml index f6b40660b..6baccd81f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,75 +5,113 @@ version: '{branch}-{build}' shallow_clone: true +cache: + - '%LOCALAPPDATA%\pip\Cache' + 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" + PYTEST_ADDOPTS: "-n auto" + # Note: There is logic to install Python version $PYTHON_VERSION if the + # $PYTHON directory doesn't exist. $PYTHON_VERSION is visible in the job + # descriptions, but can be wrong in the minor version, since we use the + # version pre-installed on AppVeyor. + # matrix: - - JOB: "2.7 32-bit" - TOXENV: "py27" - PYTHON: "C:\\Python27.11" - PYTHON_VERSION: "2.7.11" - PYTHON_ARCH: "32" - - JOB: "2.7 64-bit" TOXENV: "py27" - PYTHON: "C:\\Python27.11-x64" - PYTHON_VERSION: "2.7.11" + PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.18" PYTHON_ARCH: "64" - - JOB: "3.4 32-bit" - TOXENV: "py34" - PYTHON: "C:\\Python34" - PYTHON_VERSION: "3.4" - PYTHON_ARCH: "32" + - JOB: "3.5 64-bit" + TOXENV: "py35" + PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.9" + PYTHON_ARCH: "64" + + - JOB: "3.6 64-bit" + TOXENV: "py36" + PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.11" + PYTHON_ARCH: "64" + + - JOB: "3.7 64-bit" + TOXENV: "py37" + PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7.8" + PYTHON_ARCH: "64" - - JOB: "3.4 64-bit" - TOXENV: "py34" - PYTHON: "C:\\Python34-x64" - PYTHON_VERSION: "3.4" + - JOB: "3.8 64-bit" + TOXENV: "py38" + PYTHON: "C:\\Python38-x64" + PYTHON_VERSION: "3.8.5" PYTHON_ARCH: "64" + - JOB: "3.9 64-bit" + TOXENV: "py39" + PYTHON: "C:\\Python39-x64" + PYTHON_VERSION: "3.9.0b5" + PYTHON_ARCH: "64" + + # 32-bit jobs don't run the tests under the Python tracer, since that should + # be exactly the same as 64-bit. + - JOB: "2.7 32-bit" + TOXENV: "py27" + PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.18" + PYTHON_ARCH: "32" + COVERAGE_NO_PYTRACER: "1" + - JOB: "3.5 32-bit" TOXENV: "py35" PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5.0" + PYTHON_VERSION: "3.5.9" PYTHON_ARCH: "32" - - - JOB: "3.5 64-bit" - TOXENV: "py35" - PYTHON: "C:\\Python35-x64" - PYTHON_VERSION: "3.5.0" - PYTHON_ARCH: "64" + COVERAGE_NO_PYTRACER: "1" - JOB: "3.6 32-bit" TOXENV: "py36" PYTHON: "C:\\Python36" - PYTHON_VERSION: "3.6.0" + PYTHON_VERSION: "3.6.11" PYTHON_ARCH: "32" + COVERAGE_NO_PYTRACER: "1" - - JOB: "3.6 64-bit" - TOXENV: "py36" - PYTHON: "C:\\Python36-x64" - PYTHON_VERSION: "3.6.0" - PYTHON_ARCH: "64" + - JOB: "3.7 32-bit" + TOXENV: "py37" + PYTHON: "C:\\Python37" + PYTHON_VERSION: "3.7.8" + PYTHON_ARCH: "32" + COVERAGE_NO_PYTRACER: "1" + + - JOB: "3.8 32-bit" + TOXENV: "py38" + PYTHON: "C:\\Python38" + PYTHON_VERSION: "3.8.5" + PYTHON_ARCH: "32" + COVERAGE_NO_PYTRACER: "1" + + - JOB: "3.9 32-bit" + TOXENV: "py39" + PYTHON: "C:\\Python39" + PYTHON_VERSION: "3.9.0b5" + PYTHON_ARCH: "32" + COVERAGE_NO_PYTRACER: "1" # Meta coverage - JOB: "Meta 2.7" TOXENV: "py27" PYTHON: "C:\\Python27" - PYTHON_VERSION: "2.7" + PYTHON_VERSION: "2.7.18" PYTHON_ARCH: "32" COVERAGE_COVERAGE: "yes" - - JOB: "Meta 3.5" - TOXENV: "py35" - PYTHON: "C:\\Python35" - PYTHON_VERSION: "3.5" + - JOB: "Meta 3.6" + TOXENV: "py36" + PYTHON: "C:\\Python36" + PYTHON_VERSION: "3.6.11" PYTHON_ARCH: "32" COVERAGE_COVERAGE: "yes" @@ -91,25 +129,22 @@ install: - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" # Check that we have the expected version and architecture for Python - - "python --version" - - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" + - "python -c \"import struct, sys; print('{}\\n{}-bit'.format(sys.version, struct.calcsize('P') * 8))\"" - # Upgrade to the latest version of pip to avoid it displaying warnings + # Upgrade to the right version of pip to avoid it displaying warnings # about it being out of date. - - "pip install --disable-pip-version-check --user --upgrade pip" - # And upgrade virtualenv to get the latest pip inside .tox virtualenvs. - - "pip install --disable-pip-version-check --user --upgrade virtualenv" + - "python -m pip install --disable-pip-version-check -r requirements/pip.pip" # Install requirements. - "%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. - - "python -c \"import os; open('python{0}.{1}.bat'.format(*os.environ['TOXENV'][2:]), 'w').write('@{0}\\\\python \\x25*\\n'.format(os.environ['PYTHON']))\"" + # Make a pythonX.Y.bat file in the current directory so that tox will find it + # and pythonX.Y will mean what we want it to. + - "python -c \"import os; open('python{}.{}.bat'.format(*os.environ['TOXENV'][2:]), 'w').write('@{}\\\\python \\x25*\\n'.format(os.environ['PYTHON']))\"" build_script: - # If not a metacov job, then build wheels and .exe installers. - - if NOT "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% %PYTHON%\python setup.py bdist_wheel bdist_wininst + # If not a metacov job, then build wheel installers. + - if NOT "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% %PYTHON%\python setup.py bdist_wheel # Push everything in dist\ as an artifact. - ps: if ( Test-Path 'dist' -PathType Container ) { Get-ChildItem dist\*.* | % { Push-AppveyorArtifact $_.FullName -FileName ('dist\' + $_.Name) } } @@ -123,5 +158,10 @@ after_test: - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% pip install codecov - if "%COVERAGE_COVERAGE%" == "yes" %CMD_IN_ENV% codecov -X gcov --file coverage.xml +# Uncomment this to enable RDP access to the build when done. +# https://www.appveyor.com/docs/how-to/rdp-to-build-worker/ +# on_finish: +# - ps: $blockRdp = $true; iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) + artifacts: - path: "metacov-*.zip" diff --git a/ci/download_appveyor.py b/ci/download_appveyor.py index daf6f06eb..a3d814962 100644 --- a/ci/download_appveyor.py +++ b/ci/download_appveyor.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Use the Appveyor API to download Windows artifacts.""" @@ -17,7 +17,7 @@ def make_auth_headers(): token = f.read().strip() headers = { - 'Authorization': 'Bearer {0}'.format(token), + 'Authorization': 'Bearer {}'.format(token), } return 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(" {}, {} 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 {}".format(name)) ensure_dirs(name) z.extract(name) diff --git a/ci/github_releases.py b/ci/github_releases.py new file mode 100644 index 000000000..1c7ee6047 --- /dev/null +++ b/ci/github_releases.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Upload release notes into GitHub releases. +""" + +import json +import shlex +import subprocess +import sys + +import pkg_resources +import requests + + +RELEASES_URL = "https://api.github.com/repos/{repo}/releases" + +def run_command(cmd): + """ + Run a command line (with no shell). + + Returns a tuple: + bool: true if the command succeeded. + str: the output of the command. + + """ + proc = subprocess.run( + shlex.split(cmd), + shell=False, + check=False, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + output = proc.stdout.decode("utf-8") + succeeded = proc.returncode == 0 + return succeeded, output + +def does_tag_exist(tag_name): + """ + Does `tag_name` exist as a tag in git? + """ + return run_command(f"git rev-parse --verify {tag_name}")[0] + +def check_ok(resp): + """ + Check that the Requests response object was successful. + + Raise an exception if not. + """ + if not resp: + print(f"text: {resp.text!r}") + resp.raise_for_status() + +def github_paginated(session, url): + """ + Get all the results from a paginated GitHub url. + """ + while True: + resp = session.get(url) + check_ok(resp) + yield from resp.json() + next_link = resp.links.get("next", None) + if not next_link: + break + url = next_link["url"] + +def get_releases(session, repo): + """ + Get all the releases from a name/project repo. + + Returns: + A dict mapping tag names to release dictionaries. + """ + url = RELEASES_URL.format(repo=repo) + releases = { r['tag_name']: r for r in github_paginated(session, url) } + return releases + +def release_for_relnote(relnote): + """ + Turn a release note dict into the data needed by GitHub for a release. + """ + tag = f"coverage-{relnote['version']}" + return { + "tag_name": tag, + "name": tag, + "body": relnote["text"], + "draft": False, + "prerelease": relnote["prerelease"], + } + +def create_release(session, repo, relnote): + """ + Create a new GitHub release. + """ + print(f"Creating {relnote['version']}") + data = release_for_relnote(relnote) + resp = session.post(RELEASES_URL.format(repo=repo), json=data) + check_ok(resp) + +def update_release(session, url, relnote): + """ + Update an existing GitHub release. + """ + print(f"Updating {relnote['version']}") + data = release_for_relnote(relnote) + resp = session.patch(url, json=data) + check_ok(resp) + +def update_github_releases(json_filename, repo): + """ + Read the json file, and create or update releases in GitHub. + """ + gh_session = requests.Session() + releases = get_releases(gh_session, repo) + if 0: # if you need to delete all the releases! + for release in releases.values(): + print(release["tag_name"]) + resp = gh_session.delete(release["url"]) + check_ok(resp) + return + + with open(json_filename) as jf: + relnotes = json.load(jf) + relnotes.sort(key=lambda rel: pkg_resources.parse_version(rel["version"])) + for relnote in relnotes: + tag = "coverage-" + relnote["version"] + if not does_tag_exist(tag): + continue + exists = tag in releases + if not exists: + create_release(gh_session, repo, relnote) + else: + release = releases[tag] + if release["body"] != relnote["text"]: + url = release["url"] + update_release(gh_session, url, relnote) + +if __name__ == "__main__": + update_github_releases(*sys.argv[1:]) # pylint: disable=no-value-for-parameter diff --git a/ci/install.ps1 b/ci/install.ps1 index f9934aa1f..fd5ab2202 100644 --- a/ci/install.ps1 +++ b/ci/install.ps1 @@ -66,35 +66,10 @@ function ParsePythonVersion ($python_version) { function DownloadPython ($python_version, $platform_suffix) { $major, $minor, $micro, $prerelease = ParsePythonVersion $python_version - if (($major -le 2 -and $micro -eq 0) ` - -or ($major -eq 3 -and $minor -le 2 -and $micro -eq 0) ` - ) { - $dir = "$major.$minor" - $python_version = "$major.$minor$prerelease" - } else { - $dir = "$major.$minor.$micro" - } - - if ($prerelease) { - if (($major -le 2) ` - -or ($major -eq 3 -and $minor -eq 1) ` - -or ($major -eq 3 -and $minor -eq 2) ` - -or ($major -eq 3 -and $minor -eq 3) ` - ) { - $dir = "$dir/prev" - } - } - - if (($major -le 2) -or ($major -le 3 -and $minor -le 4)) { - $ext = "msi" - if ($platform_suffix) { - $platform_suffix = ".$platform_suffix" - } - } else { - $ext = "exe" - if ($platform_suffix) { - $platform_suffix = "-$platform_suffix" - } + $dir = "$major.$minor.$micro" + $ext = "exe" + if ($platform_suffix) { + $platform_suffix = "-$platform_suffix" } $filename = "python-$python_version$platform_suffix.$ext" @@ -173,11 +148,7 @@ function InstallPip ($python_home) { function DownloadMiniconda ($python_version, $platform_suffix) { - if ($python_version -eq "3.4") { - $filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe" - } else { - $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe" - } + $filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe" $url = $MINICONDA_URL + $filename $filepath = Download $filename $url return $filepath diff --git a/ci/manylinux.sh b/ci/manylinux.sh index 574398989..1fafec9de 100755 --- a/ci/manylinux.sh +++ b/ci/manylinux.sh @@ -16,6 +16,10 @@ if [[ $action == "build" ]]; then # Compile wheels cd /io for PYBIN in /opt/python/*/bin; do + if [[ $PYBIN == *cp34* ]]; then + # manylinux docker images have Python 3.4, but we don't use it. + continue + fi "$PYBIN/pip" install -r requirements/wheel.pip "$PYBIN/python" setup.py clean -a "$PYBIN/python" setup.py bdist_wheel -d ~/wheelhouse/ @@ -30,16 +34,25 @@ if [[ $action == "build" ]]; then elif [[ $action == "test" ]]; then # Create "pythonX.Y" links for PYBIN in /opt/python/*/bin/; do + if [[ $PYBIN == *cp34* ]]; then + # manylinux docker images have Python 3.4, but we don't use it. + continue + fi PYNAME=$("$PYBIN/python" -c "import sys; print('python{0[0]}.{0[1]}'.format(sys.version_info))") ln -sf "$PYBIN/$PYNAME" /usr/local/bin/$PYNAME done # Install packages and test - TOXBIN=/opt/python/cp27-cp27m/bin - "$TOXBIN/pip" install -r /io/requirements/ci.pip + TOXBIN=/opt/python/cp36-cp36m/bin + "$TOXBIN/pip" install -r /io/requirements/tox.pip cd /io - TOXWORKDIR=.tox_linux "$TOXBIN/tox" "$@" || true + export PYTHONPYCACHEPREFIX=/opt/pyc + if [[ $1 == "meta" ]]; then + shift + export COVERAGE_COVERAGE=yes + fi + TOXWORKDIR=.tox/linux "$TOXBIN/tox" "$@" || true cd ~ else diff --git a/ci/parse_relnotes.py b/ci/parse_relnotes.py new file mode 100644 index 000000000..d19e6d60c --- /dev/null +++ b/ci/parse_relnotes.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +Parse CHANGES.md into a JSON structure. + +Run with two arguments: the .md file to parse, and the JSON file to write: + + python parse_relnotes.py CHANGES.md relnotes.json + +Every section that has something that looks like a version number in it will +be recorded as the release notes for that version. + +""" + +import json +import re +import sys + + +class TextChunkBuffer: + """Hold onto text chunks until needed.""" + def __init__(self): + self.buffer = [] + + def append(self, text): + """Add `text` to the buffer.""" + self.buffer.append(text) + + def clear(self): + """Clear the buffer.""" + self.buffer = [] + + def flush(self): + """Produce a ("text", text) tuple if there's anything here.""" + buffered = "".join(self.buffer).strip() + if buffered: + yield ("text", buffered) + self.clear() + + +def parse_md(lines): + """Parse markdown lines, producing (type, text) chunks.""" + buffer = TextChunkBuffer() + + for line in lines: + header_match = re.search(r"^(#+) (.+)$", line) + is_header = bool(header_match) + if is_header: + yield from buffer.flush() + hashes, text = header_match.groups() + yield (f"h{len(hashes)}", text) + else: + buffer.append(line) + + yield from buffer.flush() + + +def sections(parsed_data): + """Convert a stream of parsed tokens into sections with text and notes. + + Yields a stream of: + ('h-level', 'header text', 'text') + + """ + header = None + text = [] + for ttype, ttext in parsed_data: + if ttype.startswith('h'): + if header: + yield (*header, "\n".join(text)) + text = [] + header = (ttype, ttext) + elif ttype == "text": + text.append(ttext) + else: + raise Exception(f"Don't know ttype {ttype!r}") + yield (*header, "\n".join(text)) + + +def refind(regex, text): + """Find a regex in some text, and return the matched text, or None.""" + m = re.search(regex, text) + if m: + return m.group() + else: + return None + +def relnotes(mdlines): + r"""Yield (version, text) pairs from markdown lines. + + Each tuple is a separate version mentioned in the release notes. + + A version is any section with \d\.\d in the header text. + + """ + for _, htext, text in sections(parse_md(mdlines)): + version = refind(r"\d+\.\d[^ ]*", htext) + if version: + prerelease = any(c in version for c in "abc") + when = refind(r"\d+-\d+-\d+", htext) + yield { + "version": version, + "text": text, + "prerelease": prerelease, + "when": when, + } + +def parse(md_filename, json_filename): + """Main function: parse markdown and write JSON.""" + with open(md_filename) as mf: + markdown = mf.read() + with open(json_filename, "w") as jf: + json.dump(list(relnotes(markdown.splitlines(True))), jf, indent=4) + +if __name__ == "__main__": + parse(*sys.argv[1:]) # pylint: disable=no-value-for-parameter diff --git a/ci/tidelift_relnotes.py b/ci/tidelift_relnotes.py new file mode 100644 index 000000000..bc3a37d44 --- /dev/null +++ b/ci/tidelift_relnotes.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +""" +Upload release notes from a JSON file to Tidelift as Markdown chunks + +Put your Tidelift API token in a file called tidelift.token alongside this +program, for example: + + user/n3IwOpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxc2ZwE4 + +Run with two arguments: the JSON file of release notes, and the Tidelift +package name: + + python tidelift_relnotes.py relnotes.json pypi/coverage + +Every section that has something that looks like a version number in it will +be uploaded as the release notes for that version. + +""" + +import json +import os.path +import sys + +import requests + + +def update_release_note(package, version, text): + """Update the release notes for one version of a package.""" + url = f"https://api.tidelift.com/external-api/lifting/{package}/release-notes/{version}" + token_file = os.path.join(os.path.dirname(__file__), "tidelift.token") + with open(token_file) as ftoken: + token = ftoken.read().strip() + headers = { + "Authorization": f"Bearer: {token}", + } + req_args = dict(url=url, data=text.encode('utf8'), headers=headers) + result = requests.post(**req_args) + if result.status_code == 409: + result = requests.put(**req_args) + print(f"{version}: {result.status_code}") + +def upload(json_filename, package): + """Main function: parse markdown and upload to Tidelift.""" + with open(json_filename) as jf: + relnotes = json.load(jf) + for relnote in relnotes: + update_release_note(package, relnote["version"], relnote["text"]) + +if __name__ == "__main__": + upload(*sys.argv[1:]) # pylint: disable=no-value-for-parameter diff --git a/circle.yml b/circle.yml deleted file mode 100644 index a52959eff..000000000 --- a/circle.yml +++ /dev/null @@ -1,18 +0,0 @@ -# Circle CI configuration for coverage.py. -# https://circleci.com/gh/nedbat/coveragepy - -machine: - python: - version: 2.7.6 - post: - - pyenv global pypy-2.4.0 2.6.8 2.7.9 3.3.3 3.4.2 - -dependencies: - pre: - - pip install -U pip - override: - - pip install -r requirements/tox.pip - -test: - override: - - tox diff --git a/coverage/__init__.py b/coverage/__init__.py index 63f488f2f..331b304b6 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Code coverage measurement for Python. @@ -8,11 +8,12 @@ """ +import sys + from coverage.version import __version__, __url__, version_info from coverage.control import Coverage, process_startup from coverage.data import CoverageData -from coverage.debug import enable_aspectlib_maybe from coverage.misc import CoverageException from coverage.plugin import CoveragePlugin, FileTracer, FileReporter from coverage.pytracer import PyTracer @@ -20,19 +21,15 @@ # Backward compatibility. coverage = Coverage -# Possibly enable aspectlib to debug our execution. -enable_aspectlib_maybe() - # On Windows, we encode and decode deep enough that something goes wrong and # the encodings.utf_8 module is loaded and then unloaded, I don't know why. # Adding a reference here prevents it from being unloaded. Yuk. -import encodings.utf_8 +import encodings.utf_8 # pylint: disable=wrong-import-position, wrong-import-order # Because of the "from coverage.control import fooey" lines at the top of the # file, there's an entry for coverage.coverage in sys.modules, mapped to None. # This makes some inspection tools (like pydoc) unable to find the class # coverage.coverage. So remove that entry. -import sys try: del sys.modules['coverage.coverage'] except KeyError: diff --git a/coverage/__main__.py b/coverage/__main__.py index 35ab87a56..79aa4e2b3 100644 --- a/coverage/__main__.py +++ b/coverage/__main__.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Coverage.py's main entry point.""" diff --git a/coverage/annotate.py b/coverage/annotate.py index 4060450ff..999ab6e55 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Source file annotation for coverage.py.""" @@ -8,13 +8,13 @@ import re from coverage.files import flat_rootname -from coverage.misc import isolate_module -from coverage.report import Reporter +from coverage.misc import ensure_dir, isolate_module +from coverage.report import get_analysis_to_report os = isolate_module(os) -class AnnotateReporter(Reporter): +class AnnotateReporter(object): """Generate annotated source files showing line coverage. This reporter creates annotated copies of the measured source files. Each @@ -36,8 +36,9 @@ class AnnotateReporter(Reporter): """ - def __init__(self, coverage, config): - super(AnnotateReporter, self).__init__(coverage, config) + def __init__(self, coverage): + self.coverage = coverage + self.config = self.coverage.config self.directory = None blank_re = re.compile(r"\s*(#|$)") @@ -49,7 +50,10 @@ def report(self, morfs, directory=None): See `coverage.report()` for arguments. """ - self.report_files(self.annotate_file, morfs, directory) + self.directory = directory + self.coverage.get_data() + for fr, analysis in get_analysis_to_report(self.coverage, morfs): + self.annotate_file(fr, analysis) def annotate_file(self, fr, analysis): """Annotate a single file. @@ -62,6 +66,7 @@ def annotate_file(self, fr, analysis): excluded = sorted(analysis.excluded) if self.directory: + ensure_dir(self.directory) dest_file = os.path.join(self.directory, flat_rootname(fr.relative_filename())) if dest_file.endswith("_py"): dest_file = dest_file[:-3] + ".py" diff --git a/coverage/backunittest.py b/coverage/backunittest.py index 09574ccb6..123bb2a13 100644 --- a/coverage/backunittest.py +++ b/coverage/backunittest.py @@ -1,14 +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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Implementations of unittest features from the future.""" -# Use unittest2 if it's available, otherwise unittest. This gives us -# back-ported features for 2.6. -try: - import unittest2 as unittest -except ImportError: - import unittest +import unittest def unittest_has(method): @@ -23,15 +18,11 @@ class TestCase(unittest.TestCase): `unittest` doesn't have them. """ - # pylint: disable=missing-docstring - - # Many Pythons have this method defined. But PyPy3 has a bug with it - # somehow (https://bitbucket.org/pypy/pypy/issues/2092), so always use our - # own implementation that works everywhere, at least for the ways we're - # calling it. - def assertCountEqual(self, s1, s2): - """Assert these have the same elements, regardless of order.""" - self.assertEqual(sorted(s1), sorted(s2)) + # pylint: disable=signature-differs, deprecated-method + + if not unittest_has('assertCountEqual'): + def assertCountEqual(self, *args, **kwargs): + return self.assertItemsEqual(*args, **kwargs) if not unittest_has('assertRaisesRegex'): def assertRaisesRegex(self, *args, **kwargs): diff --git a/coverage/backward.py b/coverage/backward.py index 5aff6406c..9d1d78e5b 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -1,13 +1,16 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Add things to old Pythons so I can pretend they are newer.""" -# This file does tricky stuff, so disable a pylint warning. +# This file's purpose is to provide modules to be imported from here. # pylint: disable=unused-import +import os import sys +from datetime import datetime + from coverage import env @@ -38,18 +41,32 @@ except NameError: unicode_class = str -# Where do pickles come from? -try: - import cPickle as pickle -except ImportError: - import pickle - # range or xrange? try: range = xrange # pylint: disable=redefined-builtin except NameError: range = range +try: + from itertools import zip_longest +except ImportError: + from itertools import izip_longest as zip_longest + +# Where do we get the thread id from? +try: + from thread import get_ident as get_thread_id +except ImportError: + from threading import get_ident as get_thread_id + +try: + os.PathLike +except AttributeError: + # This is Python 2 and 3 + path_types = (bytes, string_class, unicode_class) +else: + # 3.6+ + path_types = (bytes, str, os.PathLike) + # shlex.quote is new, but there's an undocumented implementation in "pipes", # who knew!? try: @@ -59,6 +76,11 @@ # in Python versions earlier than 3.3. from pipes import quote as shlex_quote +try: + import reprlib +except ImportError: + import repr as reprlib + # A function to iterate listlessly over a dict's items, and one to get the # items as a list. try: @@ -101,10 +123,18 @@ def to_bytes(s): """Convert string `s` to bytes.""" return s.encode('utf8') + def to_string(b): + """Convert bytes `b` to string.""" + return b.decode('utf8') + def binary_bytes(byte_values): """Produce a byte string with the ints from `byte_values`.""" return bytes(byte_values) + def byte_to_int(byte): + """Turn a byte indexed from a bytes object into an int.""" + return byte + def bytes_to_ints(bytes_value): """Turn a bytes object into a sequence of ints.""" # In Python 3, iterating bytes gives ints. @@ -115,10 +145,18 @@ def to_bytes(s): """Convert string `s` to bytes (no-op in 2.x).""" return s + def to_string(b): + """Convert bytes `b` to string.""" + return b + def binary_bytes(byte_values): """Produce a byte string with the ints from `byte_values`.""" return "".join(chr(b) for b in byte_values) + def byte_to_int(byte): + """Turn a byte indexed from a bytes object into an int.""" + return ord(byte) + def bytes_to_ints(bytes_value): """Turn a bytes object into a sequence of ints.""" for byte in bytes_value: @@ -155,6 +193,43 @@ def bytes_to_ints(bytes_value): PYC_MAGIC_NUMBER = imp.get_magic() +def code_object(fn): + """Get the code object from a function.""" + try: + return fn.func_code + except AttributeError: + return fn.__code__ + + +try: + from types import SimpleNamespace +except ImportError: + # The code from https://docs.python.org/3/library/types.html#types.SimpleNamespace + class SimpleNamespace: + """Python implementation of SimpleNamespace, for Python 2.""" + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + def __repr__(self): + keys = sorted(self.__dict__) + items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) + return "{}({})".format(type(self).__name__, ", ".join(items)) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +def format_local_datetime(dt): + """Return a string with local timezone representing the date. + If python version is lower than 3.6, the time zone is not included. + """ + try: + return dt.astimezone().strftime('%Y-%m-%d %H:%M %z') + except (TypeError, ValueError): + # Datetime.astimezone in Python 3.5 can not handle naive datetime + return dt.strftime('%Y-%m-%d %H:%M') + + def invalidate_import_caches(): """Invalidate any import caches that may or may not exist.""" if importlib and hasattr(importlib, "invalidate_caches"): @@ -177,6 +252,7 @@ def import_local_file(modname, modfile=None): if modfile is None: modfile = modname + '.py' if SourceFileLoader: + # pylint: disable=no-value-for-parameter, deprecated-method mod = SourceFileLoader(modname, modfile).load_module() else: for suff in imp.get_suffixes(): # pragma: part covered diff --git a/coverage/bytecode.py b/coverage/bytecode.py index d823c67c9..ceb18cf37 100644 --- a/coverage/bytecode.py +++ b/coverage/bytecode.py @@ -1,22 +1,19 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Bytecode manipulation for coverage.py""" import types -class CodeObjects(object): +def code_objects(code): """Iterate over all the code objects in `code`.""" - def __init__(self, code): - self.stack = [code] - - def __iter__(self): - while self.stack: - # We're going to return the code object on the stack, but first - # push its children for later returning. - code = self.stack.pop() - for c in code.co_consts: - if isinstance(c, types.CodeType): - self.stack.append(c) - yield code + stack = [code] + while stack: + # We're going to return the code object on the stack, but first + # push its children for later returning. + code = stack.pop() + for c in code.co_consts: + if isinstance(c, types.CodeType): + stack.append(c) + yield code diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 7b86054e3..9c9ae868a 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Command-line support for coverage.py.""" @@ -8,15 +8,19 @@ import glob import optparse import os.path +import shlex import sys import textwrap import traceback +import coverage +from coverage import Coverage from coverage import env from coverage.collector import CTracer -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.data import line_counts +from coverage.debug import info_formatter, info_header, short_stack +from coverage.execfile import PyRunner +from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource, output_encoding from coverage.results import should_fail_under @@ -42,9 +46,13 @@ class Opts(object): "Valid values are: %s." ) % ", ".join(CONCURRENCY_CHOICES), ) + context = optparse.make_option( + '', '--context', action='store', metavar="LABEL", + help="The context label to record for this coverage run.", + ) debug = optparse.make_option( '', '--debug', action='store', metavar="OPTS", - help="Debug options, separated by commas", + help="Debug options, separated by commas. [env: COVERAGE_DEBUG]", ) directory = optparse.make_option( '-d', '--directory', action='store', metavar="DIR", @@ -77,6 +85,11 @@ class Opts(object): "which isn't done by default." ), ) + sort = optparse.make_option( + '--sort', action='store', metavar='COLUMN', + help="Sort the report by the named column: name, stmts, miss, branch, brpart, or cover. " + "Default is name." + ) show_missing = optparse.make_option( '-m', '--show-missing', action='store_true', help="Show line numbers of statements in each module that weren't executed.", @@ -85,6 +98,18 @@ class Opts(object): '--skip-covered', action='store_true', help="Skip files with 100% coverage.", ) + no_skip_covered = optparse.make_option( + '--no-skip-covered', action='store_false', dest='skip_covered', + help="Disable --skip-covered.", + ) + skip_empty = optparse.make_option( + '--skip-empty', action='store_true', + help="Skip files with no code.", + ) + show_contexts = optparse.make_option( + '--show-contexts', action='store_true', + help="Show contexts for covered lines.", + ) omit = optparse.make_option( '', '--omit', action='store', metavar="PAT1,PAT2,...", @@ -93,11 +118,28 @@ class Opts(object): "Accepts shell-style wildcards, which must be quoted." ), ) + contexts = optparse.make_option( + '', '--contexts', action='store', + metavar="REGEX1,REGEX2,...", + help=( + "Only display data from lines covered in the given contexts. " + "Accepts Python regexes, which must be quoted." + ), + ) output_xml = optparse.make_option( '-o', '', action='store', dest="outfile", metavar="OUTFILE", help="Write the XML report to this file. Defaults to 'coverage.xml'", ) + output_json = optparse.make_option( + '-o', '', action='store', dest="outfile", + metavar="OUTFILE", + help="Write the JSON report to this file. Defaults to 'coverage.json'", + ) + json_pretty_print = optparse.make_option( + '', '--pretty-print', action='store_true', + help="Format the JSON for human readers.", + ) parallel_mode = optparse.make_option( '-p', '--parallel-mode', action='store_true', help=( @@ -113,9 +155,20 @@ class Opts(object): "to be run as 'python -m' would run it." ), ) + precision = optparse.make_option( + '', '--precision', action='store', metavar='N', type=int, + help=( + "Number of digits after the decimal point to display for " + "reported coverage percentages." + ), + ) rcfile = optparse.make_option( '', '--rcfile', action='store', - help="Specify configuration file. Defaults to '.coveragerc'", + help=( + "Specify configuration file. " + "By default '.coveragerc', 'setup.cfg', 'tox.ini', and " + "'pyproject.toml' are tried. [env: COVERAGE_RCFILE]" + ), ) source = optparse.make_option( '', '--source', action='store', metavar="SRC1,SRC2,...", @@ -124,7 +177,7 @@ class Opts(object): timid = optparse.make_option( '', '--timid', action='store_true', help=( - "Use a simpler but slower trace method. Try this if you get " + "Use a simpler but slower trace method. Try this if you get " "seemingly impossible results!" ), ) @@ -155,6 +208,7 @@ def __init__(self, *args, **kwargs): append=None, branch=None, concurrency=None, + context=None, debug=None, directory=None, fail_under=None, @@ -163,11 +217,16 @@ def __init__(self, *args, **kwargs): include=None, module=None, omit=None, + contexts=None, parallel_mode=None, + precision=None, pylib=None, rcfile=True, show_missing=None, skip_covered=None, + skip_empty=None, + show_contexts=None, + sort=None, source=None, timid=None, title=None, @@ -175,11 +234,6 @@ def __init__(self, *args, **kwargs): ) self.disable_interspersed_args() - self.help_fn = self.help_noop - - def help_noop(self, error=None, topic=None, parser=None): - """No-op help function.""" - pass class OptionParserError(Exception): """Used to stop the optparse error handler ending the process.""" @@ -192,15 +246,14 @@ def parse_args_ok(self, args=None, options=None): """ try: - options, args = \ - super(CoverageOptionParser, self).parse_args(args, options) + options, args = super(CoverageOptionParser, self).parse_args(args, options) except self.OptionParserError: return False, None, None return True, options, args def error(self, msg): """Override optparse.error so sys.exit doesn't get called.""" - self.help_fn(msg) + show_help(msg) raise self.OptionParserError @@ -296,10 +349,13 @@ def get_prog_name(self): "debug", GLOBAL_ARGS, usage="", description=( - "Display information on the internals of coverage.py, " + "Display information about the internals of coverage.py, " "for diagnosing problems. " - "Topics are 'data' to show a summary of the collected data, " - "or 'sys' to show installation information." + "Topics are: " + "'data' to show a summary of the collected data; " + "'sys' to show installation information; " + "'config' to show the configuration; " + "'premain' to show what is calling coverage." ), ), @@ -317,13 +373,18 @@ def get_prog_name(self): 'html': CmdOptionParser( "html", [ + Opts.contexts, Opts.directory, Opts.fail_under, Opts.ignore_errors, Opts.include, Opts.omit, - Opts.title, + Opts.precision, + Opts.show_contexts, Opts.skip_covered, + Opts.no_skip_covered, + Opts.skip_empty, + Opts.title, ] + GLOBAL_ARGS, usage="[options] [modules]", description=( @@ -333,15 +394,36 @@ def get_prog_name(self): ), ), + 'json': CmdOptionParser( + "json", + [ + Opts.contexts, + Opts.fail_under, + Opts.ignore_errors, + Opts.include, + Opts.omit, + Opts.output_json, + Opts.json_pretty_print, + Opts.show_contexts, + ] + GLOBAL_ARGS, + usage="[options] [modules]", + description="Generate a JSON report of coverage results." + ), + 'report': CmdOptionParser( "report", [ + Opts.contexts, Opts.fail_under, Opts.ignore_errors, Opts.include, Opts.omit, + Opts.precision, + Opts.sort, Opts.show_missing, Opts.skip_covered, + Opts.no_skip_covered, + Opts.skip_empty, ] + GLOBAL_ARGS, usage="[options] [modules]", description="Report coverage statistics on modules." @@ -353,6 +435,7 @@ def get_prog_name(self): Opts.append, Opts.branch, Opts.concurrency, + Opts.context, Opts.include, Opts.module, Opts.omit, @@ -373,6 +456,7 @@ def get_prog_name(self): Opts.include, Opts.omit, Opts.output_xml, + Opts.skip_empty, ] + GLOBAL_ARGS, usage="[options] [modules]", description="Generate an XML report of coverage results." @@ -380,45 +464,57 @@ def get_prog_name(self): } +def show_help(error=None, topic=None, parser=None): + """Display an error message, or the named topic.""" + assert error or topic or parser + + program_path = sys.argv[0] + if program_path.endswith(os.path.sep + '__main__.py'): + # The path is the main module of a package; get that path instead. + program_path = os.path.dirname(program_path) + program_name = os.path.basename(program_path) + if env.WINDOWS: + # entry_points={'console_scripts':...} on Windows makes files + # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These + # invoke coverage-script.py, coverage3-script.py, and + # coverage-3.5-script.py. argv[0] is the .py file, but we want to + # get back to the original form. + auto_suffix = "-script.py" + if program_name.endswith(auto_suffix): + program_name = program_name[:-len(auto_suffix)] + + help_params = dict(coverage.__dict__) + help_params['program_name'] = program_name + if CTracer is not None: + help_params['extension_modifier'] = 'with C extension' + else: + help_params['extension_modifier'] = 'without C extension' + + if error: + print(error, file=sys.stderr) + print("Use '%s help' for help." % (program_name,), file=sys.stderr) + elif parser: + print(parser.format_help().strip()) + print() + else: + help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() + if help_msg: + print(help_msg.format(**help_params)) + else: + print("Don't know topic %r" % topic) + print("Full documentation is at {__url__}".format(**help_params)) + + OK, ERR, FAIL_UNDER = 0, 1, 2 class CoverageScript(object): """The command-line interface to coverage.py.""" - def __init__(self, _covpkg=None, _run_python_file=None, - _run_python_module=None, _help_fn=None, _path_exists=None): - # _covpkg is for dependency injection, so we can test this code. - if _covpkg: - self.covpkg = _covpkg - else: - import coverage - self.covpkg = coverage - - # For dependency injection: - self.run_python_file = _run_python_file or run_python_file - self.run_python_module = _run_python_module or run_python_module - self.help_fn = _help_fn or self.help - self.path_exists = _path_exists or os.path.exists + def __init__(self): self.global_option = False - self.coverage = None - program_path = sys.argv[0] - if program_path.endswith(os.path.sep + '__main__.py'): - # The path is the main module of a package; get that path instead. - program_path = os.path.dirname(program_path) - self.program_name = os.path.basename(program_path) - if env.WINDOWS: - # entry_points={'console_scripts':...} on Windows makes files - # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These - # invoke coverage-script.py, coverage3-script.py, and - # coverage-3.5-script.py. argv[0] is the .py file, but we want to - # get back to the original form. - auto_suffix = "-script.py" - if self.program_name.endswith(auto_suffix): - self.program_name = self.program_name[:-len(auto_suffix)] - def command_line(self, argv): """The bulk of the command line interface to coverage.py. @@ -429,7 +525,7 @@ def command_line(self, argv): """ # Collect the command-line options. if not argv: - self.help_fn(topic='minimum_help') + show_help(topic='minimum_help') return OK # The command syntax we parse depends on the first argument. Global @@ -440,11 +536,10 @@ def command_line(self, argv): else: parser = CMDS.get(argv[0]) if not parser: - self.help_fn("Unknown command: '%s'" % argv[0]) + show_help("Unknown command: '%s'" % argv[0]) return ERR argv = argv[1:] - parser.help_fn = self.help_fn ok, options, args = parser.parse_args_ok(argv) if not ok: return ERR @@ -453,18 +548,15 @@ def command_line(self, argv): if self.do_help(options, args, parser): return OK - # We need to be able to import from the current directory, because - # plugins may try to, for example, to read Django settings. - sys.path[0] = '' - # Listify the list options. source = unshell_list(options.source) omit = unshell_list(options.omit) include = unshell_list(options.include) debug = unshell_list(options.debug) + contexts = unshell_list(options.contexts) # Do something. - self.coverage = self.covpkg.Coverage( + self.coverage = Coverage( data_suffix=options.parallel_mode, cover_pylib=options.pylib, timid=options.timid, @@ -475,6 +567,8 @@ def command_line(self, argv): include=include, debug=debug, concurrency=options.concurrency, + check_preimported=True, + context=options.context, ) if options.action == "debug": @@ -501,25 +595,51 @@ def command_line(self, argv): ignore_errors=options.ignore_errors, omit=omit, include=include, + contexts=contexts, ) + # We need to be able to import from the current directory, because + # plugins may try to, for example, to read Django settings. + sys.path.insert(0, '') + self.coverage.load() total = None if options.action == "report": total = self.coverage.report( show_missing=options.show_missing, - skip_covered=options.skip_covered, **report_args) + skip_covered=options.skip_covered, + skip_empty=options.skip_empty, + precision=options.precision, + sort=options.sort, + **report_args + ) elif options.action == "annotate": - self.coverage.annotate( - directory=options.directory, **report_args) + self.coverage.annotate(directory=options.directory, **report_args) elif options.action == "html": total = self.coverage.html_report( - directory=options.directory, title=options.title, - skip_covered=options.skip_covered, **report_args) + directory=options.directory, + title=options.title, + skip_covered=options.skip_covered, + skip_empty=options.skip_empty, + show_contexts=options.show_contexts, + precision=options.precision, + **report_args + ) elif options.action == "xml": outfile = options.outfile - total = self.coverage.xml_report(outfile=outfile, **report_args) + total = self.coverage.xml_report( + outfile=outfile, skip_empty=options.skip_empty, + **report_args + ) + elif options.action == "json": + outfile = options.outfile + total = self.coverage.json_report( + outfile=outfile, + pretty_print=options.pretty_print, + show_contexts=options.show_contexts, + **report_args + ) if total is not None: # Apply the command line fail-under options, and then use the config @@ -530,31 +650,14 @@ def command_line(self, argv): fail_under = self.coverage.get_option("report:fail_under") precision = self.coverage.get_option("report:precision") if should_fail_under(total, fail_under, precision): + msg = "total of {total:.{p}f} is less than fail-under={fail_under:.{p}f}".format( + total=total, fail_under=fail_under, p=precision, + ) + print("Coverage failure:", msg) return FAIL_UNDER return OK - def help(self, error=None, topic=None, parser=None): - """Display an error message, or the named topic.""" - assert error or topic or parser - if error: - 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: - help_params = dict(self.covpkg.__dict__) - help_params['program_name'] = self.program_name - if CTracer is not None: - help_params['extension_modifier'] = 'with C extension' - else: - help_params['extension_modifier'] = 'without C extension' - help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip() - if help_msg: - print(help_msg.format(**help_params)) - else: - print("Don't know topic %r" % topic) - def do_help(self, options, args, parser): """Deal with help requests. @@ -564,9 +667,9 @@ def do_help(self, options, args, parser): # Handle help. if options.help: if self.global_option: - self.help_fn(topic='help') + show_help(topic='help') else: - self.help_fn(parser=parser) + show_help(parser=parser) return True if options.action == "help": @@ -574,16 +677,16 @@ def do_help(self, options, args, parser): for a in args: parser = CMDS.get(a) if parser: - self.help_fn(parser=parser) + show_help(parser=parser) else: - self.help_fn(topic=a) + show_help(topic=a) else: - self.help_fn(topic='help') + show_help(topic='help') return True # Handle version. if options.version: - self.help_fn(topic='version') + show_help(topic='version') return True return False @@ -592,11 +695,22 @@ def do_run(self, options, args): """Implementation of 'coverage run'.""" if not args: - self.help_fn("Nothing to do.") + if options.module: + # Specified -m with nothing else. + show_help("No module specified for -m") + return ERR + command_line = self.coverage.get_option("run:command_line") + if command_line is not None: + args = shlex.split(command_line) + if args and args[0] == "-m": + options.module = True + args = args[1:] + if not args: + show_help("Nothing to do.") return ERR if options.append and self.coverage.get_option("run:parallel"): - self.help_fn("Can't append to data files in parallel mode.") + show_help("Can't append to data files in parallel mode.") return ERR if options.concurrency == "multiprocessing": @@ -606,35 +720,30 @@ def do_run(self, options, args): # As it happens, all of these options have no default, meaning # they will be None if they have not been specified. if getattr(options, opt_name) is not None: - self.help_fn( - "Options affecting multiprocessing must be specified " - "in a configuration file." + show_help( + "Options affecting multiprocessing must only be specified " + "in a configuration file.\n" + "Remove --{} from the command line.".format(opt_name) ) return ERR - if not self.coverage.get_option("run:parallel"): - if not options.append: - self.coverage.erase() + runner = PyRunner(args, as_module=bool(options.module)) + runner.prepare() + + if options.append: + self.coverage.load() # Run the script. self.coverage.start() code_ran = True try: - if options.module: - self.run_python_module(args[0], args) - else: - filename = args[0] - self.run_python_file(filename, args) + runner.run() except NoSource: code_ran = False raise finally: self.coverage.stop() if code_ran: - if options.append: - data_file = self.coverage.get_option("run:data_file") - if self.path_exists(data_file): - self.coverage.combine(data_paths=[data_file]) self.coverage.save() return OK @@ -643,7 +752,7 @@ def do_debug(self, args): """Implementation of 'coverage debug'.""" if not args: - self.help_fn("What information would you like: config, data, sys?") + show_help("What information would you like: config, data, sys, premain?") return ERR for info in args: @@ -654,12 +763,12 @@ def do_debug(self, args): print(" %s" % line) elif info == 'data': self.coverage.load() - data = self.coverage.data + data = self.coverage.get_data() print(info_header("data")) - print("path: %s" % self.coverage.data_files.filename) + print("path: %s" % self.coverage.get_data().data_filename()) if data: print("has_arcs: %r" % data.has_arcs()) - summary = data.line_counts(fullpath=True) + summary = line_counts(data, fullpath=True) filenames = sorted(summary.keys()) print("\n%d files:" % len(filenames)) for f in filenames: @@ -675,8 +784,11 @@ def do_debug(self, args): config_info = self.coverage.config.__dict__.items() for line in info_formatter(config_info): print(" %s" % line) + elif info == "premain": + print(info_header("premain")) + print(short_stack()) else: - self.help_fn("Don't know what you mean by %r" % info) + show_help("Don't know what you mean by %r" % info) return ERR return OK @@ -719,24 +831,24 @@ def unglob_args(args): Commands: annotate Annotate source files with execution information. combine Combine a number of data files. + debug Display information about the internals of coverage.py erase Erase previously collected coverage data. help Get help on using coverage.py. html Create an HTML report. + json Create a JSON report of coverage results. report Report coverage stats on modules. run Run a Python program and measure code execution. xml Create an XML report of coverage results. Use "{program_name} help " for detailed help on any command. - For full documentation, see {__url__} """, 'minimum_help': """\ - Code coverage for Python. Use '{program_name} help' for help. + Code coverage for Python, version {__version__} {extension_modifier}. Use '{program_name} help' for help. """, 'version': """\ Coverage.py, version {__version__} {extension_modifier} - Documentation at {__url__} """, } @@ -759,7 +871,10 @@ def main(argv=None): status = ERR except BaseCoverageException as err: # A controlled error inside coverage.py: print the message to the user. - print(err) + msg = err.args[0] + if env.PY2: + msg = msg.encode(output_encoding()) + print(msg) status = ERR except SystemExit as err: # The user called `sys.exit()`. Exit with their argument, if any. @@ -768,3 +883,22 @@ def main(argv=None): else: status = None return status + +# Profiling using ox_profile. Install it from GitHub: +# pip install git+https://github.com/emin63/ox_profile.git +# +# $set_env.py: COVERAGE_PROFILE - Set to use ox_profile. +_profile = os.environ.get("COVERAGE_PROFILE", "") +if _profile: # pragma: debugging + from ox_profile.core.launchers import SimpleLauncher # pylint: disable=import-error + original_main = main + + def main(argv=None): # pylint: disable=function-redefined + """A wrapper around main that profiles.""" + profiler = SimpleLauncher.launch() + try: + return original_main(argv) + finally: + data, _ = profiler.query(re_filter='coverage', max_records=100) + print(profiler.show(query=data, limit=100, sep='', col='')) + profiler.cancel() diff --git a/coverage/collector.py b/coverage/collector.py index 72ab32b61..a042357f6 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Raw data collector for coverage.py.""" @@ -9,7 +9,7 @@ from coverage import env from coverage.backward import litems, range # pylint: disable=redefined-builtin from coverage.debug import short_stack -from coverage.files import abs_file +from coverage.disposition import FileDisposition from coverage.misc import CoverageException, isolate_module from coverage.pytracer import PyTracer @@ -33,19 +33,6 @@ CTracer = None -class FileDisposition(object): - """A simple value type for recording what to do with a file.""" - pass - - -def should_start_context(frame): - """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" - fn_name = frame.f_code.co_name - if fn_name.startswith("test"): - return fn_name - return None - - class Collector(object): """Collects trace data. @@ -70,7 +57,10 @@ class Collector(object): # The concurrency settings we support here. SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) - def __init__(self, should_trace, check_include, timid, branch, warn, concurrency): + def __init__( + self, should_trace, check_include, should_start_context, file_mapper, + timid, branch, warn, concurrency, + ): """Create a collector. `should_trace` is a function, taking a file name and a frame, and @@ -79,6 +69,15 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency `check_include` is a function taking a file name and a frame. It returns a boolean: True if the file should be traced, False if not. + `should_start_context` is a function taking a frame, and returning a + string. If the frame should be the start of a new context, the string + is the new context. If the frame should not be the start of a new + context, return None. + + `file_mapper` is a function taking a filename, and returning a Unicode + filename. The result is the name that will be recorded in the data + file. + If `timid` is true, then a slower simpler trace function will be used. This is important for some environments where manipulation of tracing functions make the faster more sophisticated trace function not @@ -100,13 +99,19 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency """ self.should_trace = should_trace self.check_include = check_include + self.should_start_context = should_start_context + self.file_mapper = file_mapper self.warn = warn self.branch = branch self.threading = None + self.covdata = None + + self.static_context = None self.origin = short_stack() self.concur_id_func = None + self.mapped_file_cache = {} # We can handle a few concurrency options here, but only one at a time. these_concurrencies = self.SUPPORTED_CONCURRENCIES.intersection(concurrency) @@ -139,10 +144,6 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency ) ) - # Who-Tests-What is just a hack at the moment, so turn it on with an - # environment variable. - self.wtw = int(os.getenv('COVERAGE_WTW', 0)) - self.reset() if timid: @@ -163,13 +164,23 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency def __repr__(self): return "" % (id(self), self.tracer_name()) + def use_data(self, covdata, context): + """Use `covdata` for recording data.""" + self.covdata = covdata + self.static_context = context + self.covdata.set_context(self.static_context) + def tracer_name(self): """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() + # We used to used self.data.clear(), but that would remove filename + # keys and data values that were still in use higher up the stack + # when we are called as part of switch_context. + for d in self.data.values(): + d.clear() for tracer in self.tracers: tracer.reset_activity() @@ -181,10 +192,6 @@ def reset(self): # pairs as keys (if branch coverage). self.data = {} - # A dict mapping contexts to data dictionaries. - self.contexts = {} - self.contexts[None] = self.data - # A dictionary mapping file names to file tracer plugin names that will # handle them. self.file_tracers = {} @@ -246,11 +253,9 @@ def _start_tracer(self): tracer.threading = self.threading if hasattr(tracer, 'check_include'): tracer.check_include = self.check_include - if self.wtw: - if hasattr(tracer, 'should_start_context'): - tracer.should_start_context = should_start_context - if hasattr(tracer, 'switch_context'): - tracer.switch_context = self.switch_context + if hasattr(tracer, 'should_start_context'): + tracer.should_start_context = self.should_start_context + tracer.switch_context = self.switch_context fn = tracer.start() self.tracers.append(tracer) @@ -366,52 +371,59 @@ def _activity(self): 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 - # tracers to use it. - data = self.contexts.setdefault(new_context, {}) - for tracer in self.tracers: - tracer.data = data + """Switch to a new dynamic context.""" + self.flush_data() + if self.static_context: + context = self.static_context + if new_context: + context += "|" + new_context + else: + context = new_context + self.covdata.set_context(context) + + def cached_mapped_file(self, filename): + """A locally cached version of file names mapped through file_mapper.""" + key = (type(filename), filename) + try: + return self.mapped_file_cache[key] + except KeyError: + return self.mapped_file_cache.setdefault(key, self.file_mapper(filename)) + + def mapped_file_dict(self, d): + """Return a dict like d, but with keys modified by file_mapper.""" + # The call to litems() ensures that the GIL protects the dictionary + # iterator against concurrent modifications by tracers running + # in other threads. We try three times in case of concurrent + # access, hoping to get a clean copy. + runtime_err = None + for _ in range(3): + try: + items = litems(d) + except RuntimeError as ex: + runtime_err = ex + else: + break + else: + raise runtime_err - def save_data(self, covdata): - """Save the collected data to a `CoverageData`. + return dict((self.cached_mapped_file(k), v) for k, v in items if v) + + def flush_data(self): + """Save the collected data to our associated `CoverageData`. + + Data may have also been saved along the way. This forces the + last of the data to be saved. 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`.""" - # The call to litems() ensures that the GIL protects the dictionary - # iterator against concurrent modifications by tracers running - # in other threads. We try three times in case of concurrent - # access, hoping to get a clean copy. - runtime_err = None - for _ in range(3): - try: - items = litems(d) - except RuntimeError as ex: - runtime_err = ex - else: - break - else: - raise runtime_err # pylint: disable=raising-bad-type - - return dict((abs_file(k), v) for k, v in items) - if self.branch: - covdata.add_arcs(abs_file_dict(self.data)) + self.covdata.add_arcs(self.mapped_file_dict(self.data)) else: - covdata.add_lines(abs_file_dict(self.data)) - covdata.add_file_tracers(abs_file_dict(self.file_tracers)) - - if self.wtw: - # Just a hack, so just hack it. - import pprint - out_file = "coverage_wtw_{:06}.py".format(os.getpid()) - with open(out_file, "w") as wtw_out: - pprint.pprint(self.contexts, wtw_out) + self.covdata.add_lines(self.mapped_file_dict(self.data)) + self.covdata.add_file_tracers(self.mapped_file_dict(self.file_tracers)) self._clear_data() return True diff --git a/coverage/config.py b/coverage/config.py index 7b8f2bd00..84d9758bf 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -1,15 +1,20 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Config file for coverage.py""" import collections +import copy import os +import os.path import re -import sys +from coverage import env from coverage.backward import configparser, iitems, string_class from coverage.misc import contract, CoverageException, isolate_module +from coverage.misc import substitute_variables + +from coverage.tomlconfig import TomlConfigParser, TomlDecodeError os = isolate_module(os) @@ -30,11 +35,11 @@ def __init__(self, our_file): if our_file: self.section_prefixes.append("") - def read(self, filenames): + def read(self, filenames, encoding=None): """Read a file name as UTF-8 configuration data.""" kwargs = {} - if sys.version_info >= (3, 2): - kwargs['encoding'] = "utf-8" + if env.PYVERSION >= (3, 2): + kwargs['encoding'] = encoding or "utf-8" return configparser.RawConfigParser.read(self, filenames, **kwargs) def has_option(self, section, option): @@ -67,7 +72,7 @@ def get_section(self, section): d[opt] = self.get(section, opt) return d - def get(self, section, option, *args, **kwargs): # pylint: disable=arguments-differ + def get(self, section, option, *args, **kwargs): """Get a value, replacing environment variables also. The arguments are the same as `RawConfigParser.get`, but in the found @@ -85,23 +90,7 @@ def get(self, section, option, *args, **kwargs): # pylint: disable=argume raise configparser.NoOptionError v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) - def dollar_replace(m): - """Called for each $replacement.""" - # Only one of the groups will have matched, just get its text. - word = next(w for w in m.groups() if w is not None) # pragma: part covered - if word == "$": - return "$" - else: - return os.environ.get(word, '') - - dollar_pattern = r"""(?x) # Use extended regex syntax - \$(?: # A dollar sign, then - (?P\w+) | # a plain word, - {(?P\w+)} | # or a {-wrapped word, - (?P[$]) # or a dollar sign. - ) - """ - v = re.sub(dollar_pattern, dollar_replace, v) + v = substitute_variables(v, os.environ) return v def getlist(self, section, option): @@ -172,11 +161,18 @@ class CoverageConfig(object): operation of coverage.py. """ + # pylint: disable=too-many-instance-attributes + def __init__(self): """Initialize the configuration attributes to their defaults.""" # Metadata about the config. + # We tried to read these config files. self.attempted_config_files = [] - self.config_files = [] + # We did read these config files, but maybe didn't find any content for us. + self.config_files_read = [] + # The file that gave us our configuration. + self.config_file = None + self._config_contents = None # Defaults for [run] and [report] self._include = None @@ -184,18 +180,23 @@ def __init__(self): # Defaults for [run] self.branch = False + self.command_line = None self.concurrency = None + self.context = None self.cover_pylib = False self.data_file = ".coverage" self.debug = [] self.disable_warnings = [] + self.dynamic_context = None self.note = None self.parallel = False self.plugins = [] - self.source = None + self.relative_files = False self.run_include = None self.run_omit = None + self.source = None self.timid = False + self._crash = None # Defaults for [report] self.exclude_list = DEFAULT_EXCLUDE[:] @@ -206,20 +207,29 @@ def __init__(self): self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] self.partial_list = DEFAULT_PARTIAL[:] self.precision = 0 + self.report_contexts = None self.show_missing = False self.skip_covered = False + self.skip_empty = False + self.sort = None # Defaults for [html] self.extra_css = None self.html_dir = "htmlcov" self.html_title = "Coverage report" + self.show_contexts = False # Defaults for [xml] self.xml_output = "coverage.xml" self.xml_package_depth = 99 + # Defaults for [json] + self.json_output = "coverage.json" + self.json_pretty_print = False + self.json_show_contexts = False + # Defaults for [paths] - self.paths = {} + self.paths = collections.OrderedDict() # Options for plugins self.plugin_options = {} @@ -252,17 +262,22 @@ def from_file(self, filename, our_file): coverage.py settings in it. """ + _, ext = os.path.splitext(filename) + if ext == '.toml': + cp = TomlConfigParser(our_file) + else: + cp = HandyConfigParser(our_file) + self.attempted_config_files.append(filename) - cp = HandyConfigParser(our_file) try: files_read = cp.read(filename) - except configparser.Error as err: + except (configparser.Error, TomlDecodeError) as err: raise CoverageException("Couldn't read config file %s: %s" % (filename, err)) if not files_read: return False - self.config_files.extend(files_read) + self.config_files_read.extend(map(os.path.abspath, files_read)) any_set = False try: @@ -305,9 +320,20 @@ def from_file(self, filename, our_file): # then it was used. If we're piggybacking on someone else's file, # then it was only used if we found some settings in it. if our_file: - return True + used = True else: - return any_set + used = any_set + + if used: + self.config_file = os.path.abspath(filename) + with open(filename, "rb") as f: + self._config_contents = f.read() + + return used + + def copy(self): + """Return a copy of the configuration.""" + return copy.deepcopy(self) CONFIG_FILE_OPTIONS = [ # These are *args for _set_attr_from_config_option: @@ -320,18 +346,23 @@ def from_file(self, filename, our_file): # [run] ('branch', 'run:branch', 'boolean'), + ('command_line', 'run:command_line'), ('concurrency', 'run:concurrency', 'list'), + ('context', 'run:context'), ('cover_pylib', 'run:cover_pylib', 'boolean'), ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), ('disable_warnings', 'run:disable_warnings', 'list'), + ('dynamic_context', 'run:dynamic_context'), ('note', 'run:note'), ('parallel', 'run:parallel', 'boolean'), ('plugins', 'run:plugins', 'list'), + ('relative_files', 'run:relative_files', 'boolean'), ('run_include', 'run:include', 'list'), ('run_omit', 'run:omit', 'list'), ('source', 'run:source', 'list'), ('timid', 'run:timid', 'boolean'), + ('_crash', 'run:_crash'), # [report] ('exclude_list', 'report:exclude_lines', 'regexlist'), @@ -340,20 +371,28 @@ def from_file(self, filename, our_file): ('partial_always_list', 'report:partial_branches_always', 'regexlist'), ('partial_list', 'report:partial_branches', 'regexlist'), ('precision', 'report:precision', 'int'), + ('report_contexts', 'report:contexts', 'list'), ('report_include', 'report:include', 'list'), ('report_omit', 'report:omit', 'list'), ('show_missing', 'report:show_missing', 'boolean'), ('skip_covered', 'report:skip_covered', 'boolean'), + ('skip_empty', 'report:skip_empty', 'boolean'), ('sort', 'report:sort'), # [html] ('extra_css', 'html:extra_css'), ('html_dir', 'html:directory'), ('html_title', 'html:title'), + ('show_contexts', 'html:show_contexts', 'boolean'), # [xml] ('xml_output', 'xml:output'), ('xml_package_depth', 'xml:package_depth', 'int'), + + # [json] + ('json_output', 'json:output'), + ('json_pretty_print', 'json:pretty_print', 'boolean'), + ('json_show_contexts', 'json:show_contexts', 'boolean'), ] def _set_attr_from_config_option(self, cp, attr, where, type_=''): @@ -383,6 +422,10 @@ def set_option(self, option_name, value): `value` is the new value for the option. """ + # Special-cased options. + if option_name == "paths": + self.paths = value + return # Check all the hard-coded options. for option_spec in self.CONFIG_FILE_OPTIONS: @@ -410,6 +453,10 @@ def get_option(self, option_name): Returns the value of the option. """ + # Special-cased options. + if option_name == "paths": + return self.paths + # Check all the hard-coded options. for option_spec in self.CONFIG_FILE_OPTIONS: attr, where = option_spec[:2] @@ -425,6 +472,35 @@ def get_option(self, option_name): raise CoverageException("No such option: %r" % option_name) +def config_files_to_try(config_file): + """What config files should we try to read? + + Returns a list of tuples: + (filename, is_our_file, was_file_specified) + """ + + # 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: + # No file was specified. Check COVERAGE_RCFILE. + config_file = os.environ.get('COVERAGE_RCFILE') + if config_file: + specified_file = True + if not specified_file: + # Still no file specified. Default to .coveragerc + config_file = ".coveragerc" + files_to_try = [ + (config_file, True, specified_file), + ("setup.cfg", False, False), + ("tox.ini", False, False), + ("pyproject.toml", False, False), + ] + return files_to_try + + def read_coverage_config(config_file, **kwargs): """Read the coverage.py configuration. @@ -435,10 +511,7 @@ def read_coverage_config(config_file, **kwargs): 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: config is a CoverageConfig object read from the appropriate configuration file. @@ -449,26 +522,16 @@ def read_coverage_config(config_file, **kwargs): # 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, our_file in [(config_file, True), - ("setup.cfg", False), - ("tox.ini", False)]: - config_read = config.from_file(fname, our_file=our_file) - 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) + files_to_try = config_files_to_try(config_file) + for fname, our_file, specified_file in files_to_try: + config_read = config.from_file(fname, our_file=our_file) if config_read: break + if specified_file: + raise CoverageException("Couldn't read '%s' as a config file" % fname) + # $set_env.py: COVERAGE_DEBUG - Options for --debug. # 3) from environment variables: env_data_file = os.environ.get('COVERAGE_FILE') if env_data_file: @@ -485,5 +548,9 @@ def read_coverage_config(config_file, **kwargs): config.data_file = os.path.expanduser(config.data_file) config.html_dir = os.path.expanduser(config.html_dir) config.xml_output = os.path.expanduser(config.xml_output) + config.paths = collections.OrderedDict( + (k, [os.path.expanduser(f) for f in v]) + for k, v in config.paths.items() + ) - return config_file, config + return config diff --git a/coverage/context.py b/coverage/context.py new file mode 100644 index 000000000..ea13da21e --- /dev/null +++ b/coverage/context.py @@ -0,0 +1,91 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Determine contexts for coverage.py""" + + +def combine_context_switchers(context_switchers): + """Create a single context switcher from multiple switchers. + + `context_switchers` is a list of functions that take a frame as an + argument and return a string to use as the new context label. + + Returns a function that composites `context_switchers` functions, or None + if `context_switchers` is an empty list. + + When invoked, the combined switcher calls `context_switchers` one-by-one + until a string is returned. The combined switcher returns None if all + `context_switchers` return None. + """ + if not context_switchers: + return None + + if len(context_switchers) == 1: + return context_switchers[0] + + def should_start_context(frame): + """The combiner for multiple context switchers.""" + for switcher in context_switchers: + new_context = switcher(frame) + if new_context is not None: + return new_context + return None + + return should_start_context + + +def should_start_context_test_function(frame): + """Is this frame calling a test_* function?""" + co_name = frame.f_code.co_name + if co_name.startswith("test") or co_name == "runTest": + return qualname_from_frame(frame) + return None + + +def qualname_from_frame(frame): + """Get a qualified name for the code running in `frame`.""" + co = frame.f_code + fname = co.co_name + method = None + if co.co_argcount and co.co_varnames[0] == "self": + self = frame.f_locals["self"] + method = getattr(self, fname, None) + + if method is None: + func = frame.f_globals.get(fname) + if func is None: + return None + return func.__module__ + '.' + fname + + func = getattr(method, '__func__', None) + if func is None: + cls = self.__class__ + return cls.__module__ + '.' + cls.__name__ + "." + fname + + if hasattr(func, '__qualname__'): + qname = func.__module__ + '.' + func.__qualname__ + else: + for cls in getattr(self.__class__, '__mro__', ()): + f = cls.__dict__.get(fname, None) + if f is None: + continue + if f is func: + qname = cls.__module__ + '.' + cls.__name__ + "." + fname + break + else: + # Support for old-style classes. + def mro(bases): + for base in bases: + f = base.__dict__.get(fname, None) + if f is func: + return base.__module__ + '.' + base.__name__ + "." + fname + for base in bases: + qname = mro(base.__bases__) + if qname is not None: + return qname + return None + qname = mro([self.__class__]) + if qname is None: + qname = func.__module__ + '.' + fname + + return qname diff --git a/coverage/control.py b/coverage/control.py index b82c80476..cc0c271ac 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -1,36 +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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Core control stuff for coverage.py.""" - import atexit -import inspect -import itertools +import contextlib import os +import os.path import platform -import re import sys import time -import traceback from coverage import env from coverage.annotate import AnnotateReporter from coverage.backward import string_class, iitems -from coverage.collector import Collector +from coverage.collector import Collector, CTracer from coverage.config import read_coverage_config -from coverage.data import CoverageData, CoverageDataFiles -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.context import should_start_context_test_function, combine_context_switchers +from coverage.data import CoverageData, combine_parallel_data +from coverage.debug import DebugControl, short_stack, write_formatted_info +from coverage.disposition import disposition_debug_msg +from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory from coverage.html import HtmlReporter +from coverage.inorout import InOrOut +from coverage.jsonreport import JsonReporter from coverage.misc import CoverageException, bool_or_none, join_regex -from coverage.misc import file_be_gone, isolate_module +from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module from coverage.plugin import FileReporter from coverage.plugin_support import Plugins -from coverage.python import PythonFileReporter, source_for_file +from coverage.python import PythonFileReporter +from coverage.report import render_report from coverage.results import Analysis, Numbers from coverage.summary import SummaryReporter from coverage.xmlreport import XmlReporter @@ -43,22 +42,23 @@ os = isolate_module(os) -# Pypy has some unusual stuff in the "stdlib". Consider those locations -# 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 -if env.PYPY: - try: - import _structseq - except ImportError: - pass +@contextlib.contextmanager +def override_config(cov, **kwargs): + """Temporarily tweak the configuration of `cov`. + The arguments are applied to `cov.config` with the `from_args` method. + At the end of the with-statement, the old configuration is restored. + """ + original_config = cov.config + cov.config = cov.config.copy() try: - import _pypy_irc_topic - except ImportError: - pass + cov.config.from_args(**kwargs) + yield + finally: + cov.config = original_config + +_DEFAULT_DATAFILE = DefaultValue("MISSING") class Coverage(object): """Programmatic access to coverage.py. @@ -73,18 +73,45 @@ class Coverage(object): cov.stop() cov.html_report(directory='covhtml') + Note: in keeping with Python custom, names starting with underscore are + not part of the public API. They might stop working at any point. Please + limit yourself to documented methods to avoid problems. + """ + + # The stack of started Coverage instances. + _instances = [] + + @classmethod + def current(cls): + """Get the latest started `Coverage` instance, if any. + + Returns: a `Coverage` instance, or None. + + .. versionadded:: 5.0 + + """ + if cls._instances: + return cls._instances[-1] + else: + return None + def __init__( - self, data_file=None, data_suffix=None, cover_pylib=None, + self, data_file=_DEFAULT_DATAFILE, data_suffix=None, cover_pylib=None, auto_data=False, timid=None, branch=None, config_file=True, source=None, omit=None, include=None, debug=None, - concurrency=None, + concurrency=None, check_preimported=False, context=None, ): """ - `data_file` is the base name of the data file to use, defaulting to - ".coverage". `data_suffix` is appended (with a dot) to `data_file` to - create the final file name. If `data_suffix` is simply True, then a - suffix is created with the machine and process identity included. + Many of these arguments duplicate and override values that can be + provided in a configuration file. Parameters that are missing here + will use values from the config file. + + `data_file` is the base name of the data file to use. The config value + defaults to ".coverage". None can be provided to prevent writing a data + file. `data_suffix` is appended (with a dot) to `data_file` to create + the final file name. If `data_suffix` is simply True, then a suffix is + created with the machine and process identity included. `cover_pylib` is a boolean determining whether Python code installed with the Python interpreter is measured. This includes the Python @@ -132,58 +159,72 @@ def __init__( "eventlet", "gevent", "multiprocessing", or "thread" (the default). This can also be a list of these strings. + If `check_preimported` is true, then when coverage is started, the + already-imported files will be checked to see if they should be + measured by coverage. Importing measured files before coverage is + started can mean that code is missed. + + `context` is a string to use as the :ref:`static context + ` label for collected data. + .. versionadded:: 4.0 The `concurrency` parameter. .. versionadded:: 4.2 The `concurrency` parameter can now be a list of strings. + .. versionadded:: 5.0 + The `check_preimported` and `context` parameters. + """ + # data_file=None means no disk file at all. data_file missing means + # use the value from the config file. + self._no_disk = data_file is None + if data_file is _DEFAULT_DATAFILE: + data_file = None + # Build our configuration from a number of sources. - self.config_file, self.config = read_coverage_config( + 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, run_omit=omit, run_include=include, debug=debug, report_omit=omit, report_include=include, - concurrency=concurrency, + concurrency=concurrency, context=context, ) # This is injectable by tests. self._debug_file = None self._auto_load = self._auto_save = auto_data - self._data_suffix = data_suffix - - # The matchers for _should_trace. - self.source_match = None - self.source_pkgs_match = None - self.pylib_match = self.cover_match = None - self.include_match = self.omit_match = None + self._data_suffix_specified = data_suffix # Is it ok for no data to be collected? self._warn_no_data = True self._warn_unimported_source = True + self._warn_preimported_source = check_preimported + self._no_warn_slugs = None # A record of all the warnings that have been issued. self._warnings = [] # 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_paths = self.cover_paths = None - self.data_suffix = self.run_suffix = None + self._data = self._collector = None + self._plugins = None + self._inorout = None + self._data_suffix = self._run_suffix = None self._exclude_re = None - self.debug = None + self._debug = None + self._file_mapper = None # State machine variables: # Have we initialized everything? self._inited = False + self._inited_for_start = False # Have we started collecting and not stopped it? self._started = False + # Should we write the debug output? + self._should_write_debug = True # If we have sub-process measurement happening automatically, then we # want any explicit creation of a Coverage object to mean, this process @@ -209,378 +250,61 @@ def _init(self): # Create and configure the debugging controller. COVERAGE_DEBUG_FILE # is an environment variable, the name of a file to append debug logs # to. - if self._debug_file is None: - debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE") - if debug_file_name: - self._debug_file = open(debug_file_name, "a") - else: - self._debug_file = sys.stderr - self.debug = DebugControl(self.config.debug, self._debug_file) + self._debug = DebugControl(self.config.debug, self._debug_file) + + if "multiprocessing" in (self.config.concurrency or ()): + # Multi-processing uses parallel for the subprocesses, so also use + # it for the main process. + self.config.parallel = True # _exclude_re is a dict that maps exclusion list names to compiled regexes. self._exclude_re = {} set_relative_directory() + self._file_mapper = relative_filename if self.config.relative_files else abs_file # Load plugins - self.plugins = Plugins.load_plugins(self.config.plugins, self.config, self.debug) + self._plugins = Plugins.load_plugins(self.config.plugins, self.config, self._debug) # Run configuring plugins. - for plugin in self.plugins.configurers: + for plugin in self._plugins.configurers: # We need an object with set_option and get_option. Either self or # self.config will do. Choosing randomly stops people from doing # other things with those objects, against the public API. Yes, # this is a bit childish. :) plugin.configure([self, self.config][int(time.time()) % 2]) - # The source argument can be directories or package names. - self.source = [] - self.source_pkgs = [] - for src in self.config.source or []: - 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.run_omit) - self.include = prep_patterns(self.config.run_include) + def _post_init(self): + """Stuff to do after everything is initialized.""" + if self._should_write_debug: + self._should_write_debug = False + self._write_startup_debug() - 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. - self.config.parallel = True - - self.collector = Collector( - should_trace=self._should_trace, - check_include=self._check_include_omit_etc, - timid=self.config.timid, - branch=self.config.branch, - warn=self._warn, - concurrency=concurrency, - ) - - # Early warning if we aren't going to be able to support plugins. - if self.plugins.file_tracers and not self.collector.supports_plugins: - self._warn( - "Plugin file tracers (%s) aren't supported with %s" % ( - ", ".join( - plugin._coverage_plugin_name - for plugin in self.plugins.file_tracers - ), - self.collector.tracer_name(), - ) - ) - for plugin in self.plugins.file_tracers: - plugin._coverage_enabled = False - - # Suffixes are a bit tricky. We want to use the data suffix only when - # collecting data, not when combining data. So we save it as - # `self.run_suffix` now, and promote it to `self.data_suffix` if we - # find that we are collecting data later. - if self._data_suffix or self.config.parallel: - if not isinstance(self._data_suffix, string_class): - # if data_suffix=True, use .machinename.pid.random - self._data_suffix = True - else: - self._data_suffix = None - self.data_suffix = None - self.run_suffix = self._data_suffix - - # Create the data file. We do this at construction time so that the - # 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, debug=self.debug, - ) - - # The directories for files considered "installed with the interpreter". - 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 - # environments (virtualenv, for example), these modules may be - # spread across a few locations. Look at all the candidate modules - # 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_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 - # objects still have the file names. So dig into one to find - # the path to exclude. - structseq_new = _structseq.structseq_new - try: - structseq_file = structseq_new.func_code.co_filename - except AttributeError: - structseq_file = structseq_new.__code__.co_filename - 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_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_paths.append(self._canonical_path(mod)) - - # Set the reporting precision. - Numbers.set_precision(self.config.precision) - - atexit.register(self._atexit) - - # 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_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() + # '[run] _crash' will raise an exception if the value is close by in + # the call stack, for testing error handling. + if self.config._crash and self.config._crash in short_stack(limit=4): + raise Exception("Crashing because called by {}".format(self.config._crash)) 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'): + with self._debug.without_callers(): + if self._debug.should('config'): config_info = sorted(self.config.__dict__.items()) - write_formatted_info(self.debug, "config", config_info) + config_info = [(k, v) for k, v in config_info if not k.startswith('_')] + write_formatted_info(self._debug, "config", config_info) wrote_any = True - if self.debug.should('sys'): - write_formatted_info(self.debug, "sys", self.sys_info()) - for plugin in self.plugins: + if self._debug.should('sys'): + write_formatted_info(self._debug, "sys", self.sys_info()) + for plugin in self._plugins: header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() - write_formatted_info(self.debug, header, info) + write_formatted_info(self._debug, header, info) wrote_any = True if wrote_any: - write_formatted_info(self.debug, "end", ()) - - def _canonical_path(self, morf, directory=False): - """Return the canonical path of the module or file `morf`. - - 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. - - """ - 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. - - For configurability's sake, we allow __main__ modules to be matched by - their importable name. - - If loaded via runpy (aka -m), we can usually recover the "original" - full dotted module name, otherwise, we resort to interpreting the - file name to get the module's name. In the case that the module name - 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__': - # This is the usual case: an imported module. - return dunder_name - - loader = module_globals.get('__loader__', None) - for attrname in ('fullname', 'name'): # attribute renamed in py3.2 - if hasattr(loader, attrname): - fullname = getattr(loader, attrname) - else: - continue - - if isinstance(fullname, str) and fullname != '__main__': - # Module loaded via: runpy -m - return fullname - - # Script as first argument to Python command line. - inspectedname = inspect.getmodulename(filename) - if inspectedname is not None: - return inspectedname - else: - return dunder_name - - def _should_trace_internal(self, filename, frame): - """Decide whether to trace execution in `filename`, with a reason. - - This function is called from the trace function. As each new file name - is encountered, this function determines whether it is traced or not. - - Returns a FileDisposition object. - - """ - original_filename = filename - disp = _disposition_init(self.collector.file_disposition_class, filename) - - def nope(disp, reason): - """Simple helper to make it easy to return NO.""" - disp.trace = False - disp.reason = reason - return disp - - # Compiled Python files have two file names: frame.f_code.co_filename is - # the file name at the time the .pyc was compiled. The second name is - # __file__, which is where the .pyc was actually loaded from. Since - # .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 and frame.f_globals.get('__file__') - if 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): - # Files shouldn't be renamed when moved. This happens when - # exec'ing code. If it seems like something is wrong with - # the frame's file name, then just use the original. - filename = original_filename - - if not filename: - # Empty string is pretty useless. - return nope(disp, "empty string isn't a file name") - - if filename.startswith('memory:'): - return nope(disp, "memory isn't traceable") - - if filename.startswith('<'): - # Lots of non-file execution is represented with artificial - # file names like "", "", or - # "". Don't ever trace these executions, since we - # can't do anything with the data later anyway. - return nope(disp, "not a real file name") - - # pyexpat does a dumb thing, calling the trace function explicitly from - # C code with a C file name. - if re.search(r"[/\\]Modules[/\\]pyexpat.c", filename): - return nope(disp, "pyexpat lies about itself") - - # Jython reports the .class file to the tracer, use the source file. - if filename.endswith("$py.class"): - filename = filename[:-9] + ".py" - - canonical = canonical_filename(filename) - disp.canonical_filename = canonical - - # Try the plugins, see if they have an opinion about the file. - plugin = None - for plugin in self.plugins.file_tracers: - if not plugin._coverage_enabled: - continue - - try: - file_tracer = plugin.file_tracer(canonical) - if file_tracer is not None: - file_tracer._coverage_plugin = plugin - disp.trace = True - disp.file_tracer = file_tracer - if file_tracer.has_dynamic_source_filename(): - disp.has_dynamic_filename = True - else: - disp.source_filename = canonical_filename( - file_tracer.source_filename() - ) - break - except Exception: - self._warn( - "Disabling plug-in %r due to an exception:" % ( - plugin._coverage_plugin_name - ) - ) - traceback.print_exc() - plugin._coverage_enabled = False - continue - else: - # No plugin wanted it: it's Python. - disp.trace = True - disp.source_filename = canonical - - if not disp.has_dynamic_filename: - if not disp.source_filename: - raise CoverageException( - "Plugin %r didn't set source_filename for %r" % - (plugin, disp.original_filename) - ) - reason = self._check_include_omit_etc_internal( - disp.source_filename, frame, - ) - if reason: - nope(disp, reason) - - return disp - - def _check_include_omit_etc_internal(self, filename, frame): - """Check a file name against the include, omit, etc, rules. - - Returns a string or None. String means, don't trace, and is the reason - why. None means no reason found to not trace. - - """ - modulename = self._name_for_module(frame.f_globals, filename) - - # If the user specified source or include, then that's authoritative - # about the outer bound of what to measure and we don't have to apply - # any canned exclusions. If they didn't, then we have to exclude the - # stdlib and coverage.py directories. - if self.source_match: - if self.source_pkgs_match.match(modulename): - if modulename in self.source_pkgs_unmatched: - self.source_pkgs_unmatched.remove(modulename) - elif not self.source_match.match(filename): - return "falls outside the --source trees" - elif self.include_match: - if not self.include_match.match(filename): - return "falls outside the --include trees" - else: - # If we aren't supposed to trace installed code, then check if this - # is near the Python standard library and skip it if so. - if self.pylib_match and self.pylib_match.match(filename): - return "is in the stdlib" - - # We exclude the coverage.py code itself, since a little of it - # will be measured otherwise. - if self.cover_match and self.cover_match.match(filename): - return "is part of coverage.py" - - # Check the file against the omit pattern. - if self.omit_match and self.omit_match.match(filename): - return "is inside an --omit pattern" - - # No reason found to skip this file. - return None + write_formatted_info(self._debug, "end", ()) def _should_trace(self, filename, frame): """Decide whether to trace execution in `filename`. @@ -588,9 +312,9 @@ def _should_trace(self, filename, frame): Calls `_should_trace_internal`, and returns the FileDisposition. """ - disp = self._should_trace_internal(filename, frame) - if self.debug.should('trace'): - self.debug.write(_disposition_debug_msg(disp)) + disp = self._inorout.should_trace(filename, frame) + if self._debug.should('trace'): + self._debug.write(disposition_debug_msg(disp)) return disp def _check_include_omit_etc(self, filename, frame): @@ -599,32 +323,42 @@ def _check_include_omit_etc(self, filename, frame): Returns a boolean: True if the file should be traced, False if not. """ - reason = self._check_include_omit_etc_internal(filename, frame) - if self.debug.should('trace'): + reason = self._inorout.check_include_omit_etc(filename, frame) + if self._debug.should('trace'): if not reason: msg = "Including %r" % (filename,) else: msg = "Not including %r: %s" % (filename, reason) - self.debug.write(msg) + self._debug.write(msg) return not reason - def _warn(self, msg, slug=None): + def _warn(self, msg, slug=None, once=False): """Use `msg` as a warning. For warning suppression, use `slug` as the shorthand. + + If `once` is true, only show this warning once (determined by the + slug.) + """ - if slug in self.config.disable_warnings: + if self._no_warn_slugs is None: + self._no_warn_slugs = list(self.config.disable_warnings) + + if slug in self._no_warn_slugs: # Don't issue the warning return self._warnings.append(msg) if slug: msg = "%s (%s)" % (msg, slug) - if self.debug.should('pid'): + if self._debug.should('pid'): msg = "[%d] %s" % (os.getpid(), msg) sys.stderr.write("Coverage.py warning: %s\n" % msg) + if once: + self._no_warn_slugs.append(slug) + def get_option(self, option_name): """Get an option from the configuration. @@ -632,7 +366,11 @@ def get_option(self, option_name): option name. For example, the ``branch`` option in the ``[run]`` section of the config file would be indicated with `"run:branch"`. - Returns the value of the option. + Returns the value of the option. The type depends on the option + selected. + + As a special case, an `option_name` of ``"paths"`` will return an + OrderedDict with the entire ``[paths]`` section value. .. versionadded:: 4.0 @@ -659,22 +397,118 @@ def set_option(self, option_name, value): [run] branch = True + As a special case, an `option_name` of ``"paths"`` will replace the + entire ``[paths]`` section. The value should be an OrderedDict. + .. versionadded:: 4.0 """ self.config.set_option(option_name, value) - def use_cache(self, usecache): - """Obsolete method.""" - self._init() - if not usecache: - self._warn("use_cache(False) is no longer supported.") - def load(self): """Load previously-collected coverage data from the data file.""" self._init() - self.collector.reset() - self.data_files.read(self.data) + if self._collector: + self._collector.reset() + should_skip = self.config.parallel and not os.path.exists(self.config.data_file) + if not should_skip: + self._init_data(suffix=None) + self._post_init() + if not should_skip: + self._data.read() + + def _init_for_start(self): + """Initialization for start()""" + # Construct the collector. + 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.config_file) + + dycon = self.config.dynamic_context + if not dycon or dycon == "none": + context_switchers = [] + elif dycon == "test_function": + context_switchers = [should_start_context_test_function] + else: + raise CoverageException( + "Don't understand dynamic_context setting: {!r}".format(dycon) + ) + + context_switchers.extend( + plugin.dynamic_context for plugin in self._plugins.context_switchers + ) + + should_start_context = combine_context_switchers(context_switchers) + + self._collector = Collector( + should_trace=self._should_trace, + check_include=self._check_include_omit_etc, + should_start_context=should_start_context, + file_mapper=self._file_mapper, + timid=self.config.timid, + branch=self.config.branch, + warn=self._warn, + concurrency=concurrency, + ) + + suffix = self._data_suffix_specified + if suffix or self.config.parallel: + if not isinstance(suffix, string_class): + # if data_suffix=True, use .machinename.pid.random + suffix = True + else: + suffix = None + + self._init_data(suffix) + + self._collector.use_data(self._data, self.config.context) + + # Early warning if we aren't going to be able to support plugins. + if self._plugins.file_tracers and not self._collector.supports_plugins: + self._warn( + "Plugin file tracers (%s) aren't supported with %s" % ( + ", ".join( + plugin._coverage_plugin_name + for plugin in self._plugins.file_tracers + ), + self._collector.tracer_name(), + ) + ) + for plugin in self._plugins.file_tracers: + plugin._coverage_enabled = False + + # Create the file classifying substructure. + self._inorout = InOrOut( + warn=self._warn, + debug=(self._debug if self._debug.should('trace') else None), + ) + self._inorout.configure(self.config) + self._inorout.plugins = self._plugins + self._inorout.disp_class = self._collector.file_disposition_class + + # It's useful to write debug info after initing for start. + self._should_write_debug = True + + atexit.register(self._atexit) + + def _init_data(self, suffix): + """Create a data file if we don't have one yet.""" + if self._data is None: + # Create the data file. We do this at construction time so that the + # data file will be written into the directory where the process + # started rather than wherever the process eventually chdir'd to. + ensure_dir_for_file(self.config.data_file) + self._data = CoverageData( + basename=self.config.data_file, + suffix=suffix, + warn=self._warn, + debug=self._debug, + no_disk=self._no_disk, + ) def start(self): """Start measuring code coverage. @@ -688,45 +522,82 @@ def start(self): """ self._init() - if self.include: - if self.source or self.source_pkgs: - self._warn("--include is ignored because --source is set", slug="include-ignored") - if self.run_suffix: - # Calling start() means we're running code, so use the run_suffix - # as the data_suffix when we eventually save the data. - self.data_suffix = self.run_suffix + if not self._inited_for_start: + self._inited_for_start = True + self._init_for_start() + self._post_init() + + # Issue warnings for possible problems. + self._inorout.warn_conflicting_settings() + + # See if we think some code that would eventually be measured has + # already been imported. + if self._warn_preimported_source: + self._inorout.warn_already_imported_files() + if self._auto_load: self.load() - self.collector.start() + self._collector.start() self._started = True + self._instances.append(self) def stop(self): """Stop measuring code coverage.""" + if self._instances: + if self._instances[-1] is self: + self._instances.pop() if self._started: - self.collector.stop() + self._collector.stop() self._started = False def _atexit(self): """Clean up on process shutdown.""" - if self.debug.should("process"): - self.debug.write("atexit: {0!r}".format(self)) + if self._debug.should("process"): + self._debug.write("atexit: pid: {}, instance: {!r}".format(os.getpid(), self)) if self._started: self.stop() if self._auto_save: self.save() def erase(self): - """Erase previously-collected coverage data. + """Erase previously collected coverage data. This removes the in-memory data collected in this session as well as discarding the data file. """ self._init() - self.collector.reset() - self.data.erase() - self.data_files.erase(parallel=self.config.parallel) + self._post_init() + if self._collector: + self._collector.reset() + self._init_data(suffix=None) + self._data.erase(parallel=self.config.parallel) + self._data = None + self._inited_for_start = False + + def switch_context(self, new_context): + """Switch to a new dynamic context. + + `new_context` is a string to use as the :ref:`dynamic context + ` label for collected data. If a :ref:`static + context ` is in use, the static and dynamic context + labels will be joined together with a pipe character. + + Coverage collection must be started already. + + .. versionadded:: 5.0 + + """ + if not self._started: # pragma: part started + raise CoverageException( + "Cannot switch context, coverage is not started" + ) + + if self._collector.should_start_context: + self._warn("Conflicting dynamic contexts", slug="dynamic-conflict", once=True) + + self._collector.switch_context(new_context) def clear_exclude(self, which='exclude'): """Clear the exclude list.""" @@ -777,9 +648,8 @@ def get_exclude_list(self, which='exclude'): def save(self): """Save the collected coverage data to the data file.""" - self._init() - self.get_data() - self.data_files.write(self.data, suffix=self.data_suffix) + data = self.get_data() + data.write() def combine(self, data_paths=None, strict=False): """Combine together a number of similarly-named coverage data files. @@ -804,6 +674,8 @@ def combine(self, data_paths=None, strict=False): """ self._init() + self._init_data(suffix=None) + self._post_init() self.get_data() aliases = None @@ -814,9 +686,7 @@ def combine(self, data_paths=None, strict=False): for pattern in paths[1:]: aliases.add(pattern, result) - self.data_files.combine_parallel_data( - self.data, aliases=aliases, data_paths=data_paths, strict=strict, - ) + combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict) def get_data(self): """Get the collected data. @@ -829,11 +699,13 @@ def get_data(self): """ self._init() + self._init_data(suffix=None) + self._post_init() - if self.collector.save_data(self.data): + if self._collector and self._collector.flush_data(): self._post_save_work() - return self.data + return self._data def _post_save_work(self): """After saving data, look for warnings, post-work, etc. @@ -845,82 +717,21 @@ def _post_save_work(self): # 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_unmatched: - self._warn_about_unmeasured_code(pkg) + self._inorout.warn_unimported_source() # Find out if we got any data. - if not self.data and self._warn_no_data: + if not self._data and self._warn_no_data: self._warn("No data was collected.", slug="no-data-collected") - # Find files that were never executed at all. - 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)) - - for src in self.source: - self._find_unexecuted_files(src) + # Touch all the files that could have executed, so that we can + # mark completely unexecuted files as 0% covered. + if self._data is not None: + for file_path, plugin_name in self._inorout.find_possibly_unexecuted_files(): + file_path = self._file_mapper(file_path) + self._data.touch_file(file_path, plugin_name) if self.config.note: - self.data.add_run_info(note=self.config.note) - - def _warn_about_unmeasured_code(self, pkg): - """Warn about a package or module that we never traced. - - `pkg` is a string, the name of the package or module. - - """ - mod = sys.modules.get(pkg) - if mod is None: - self._warn("Module %s was never imported." % pkg, slug="module-not-imported") - return - - is_namespace = hasattr(mod, '__path__') and not hasattr(mod, '__file__') - has_file = hasattr(mod, '__file__') and os.path.exists(mod.__file__) - - if is_namespace: - # A namespace package. It's OK for this not to have been traced, - # since there is no code directly in it. - return - - if not has_file: - self._warn("Module %s has no Python source." % pkg, slug="module-not-python") - return - - # The module was in sys.modules, and seems like a module with code, but - # we never measured it. I guess that means it was imported before - # coverage even started. - self._warn( - "Module %s was previously imported, but not measured" % pkg, - slug="module-not-measured", - ) - - def _find_plugin_files(self, src_dir): - """Get executable files from the plugins.""" - for plugin in self.plugins.file_tracers: - 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) + self._warn("The '[run] note' setting is no longer supported.") # Backward compatibility with version 1. def analysis(self, morf): @@ -945,7 +756,6 @@ def analysis2(self, morf): coverage data. """ - self._init() analysis = self._analyze(morf) return ( analysis.filename, @@ -961,11 +771,16 @@ def _analyze(self, it): Returns an `Analysis` object. """ - self.get_data() + # All reporting comes through here, so do reporting initialization. + self._init() + Numbers.set_precision(self.config.precision) + self._post_init() + + data = self.get_data() if not isinstance(it, FileReporter): it = self._get_file_reporter(it) - return Analysis(self.data, it) + return Analysis(data, it, self._file_mapper) def _get_file_reporter(self, morf): """Get a FileReporter for a module or file name.""" @@ -973,19 +788,19 @@ def _get_file_reporter(self, morf): file_reporter = "python" if isinstance(morf, string_class): - abs_morf = abs_file(morf) - plugin_name = self.data.file_tracer(abs_morf) + mapped_morf = self._file_mapper(morf) + plugin_name = self._data.file_tracer(mapped_morf) if plugin_name: - plugin = self.plugins.get(plugin_name) - - if plugin: - file_reporter = plugin.file_reporter(abs_morf) - if file_reporter is None: - raise CoverageException( - "Plugin %r did not provide a file reporter for %r." % ( - plugin._coverage_plugin_name, morf - ) - ) + plugin = self._plugins.get(plugin_name) + + if plugin: + file_reporter = plugin.file_reporter(mapped_morf) + if file_reporter is None: + raise CoverageException( + "Plugin %r did not provide a file reporter for %r." % ( + plugin._coverage_plugin_name, morf + ) + ) if file_reporter == "python": file_reporter = PythonFileReporter(morf, self) @@ -1004,49 +819,77 @@ def _get_file_reporters(self, morfs=None): """ if not morfs: - morfs = self.data.measured_files() + morfs = self._data.measured_files() - # Be sure we have a list. - if not isinstance(morfs, (list, tuple)): + # Be sure we have a collection. + if not isinstance(morfs, (list, tuple, set)): morfs = [morfs] - file_reporters = [] - for morf in morfs: - file_reporter = self._get_file_reporter(morf) - file_reporters.append(file_reporter) - + file_reporters = [self._get_file_reporter(morf) for morf in morfs] return file_reporters def report( self, morfs=None, show_missing=None, ignore_errors=None, - file=None, # pylint: disable=redefined-builtin - omit=None, include=None, skip_covered=None, + file=None, omit=None, include=None, skip_covered=None, + contexts=None, skip_empty=None, precision=None, sort=None ): - """Write a summary report to `file`. + """Write a textual summary report to `file`. Each module in `morfs` is listed, with counts of statements, executed statements, missing statements, and a list of lines missed. + If `show_missing` is true, then details of which lines or branches are + missing will be included in the report. If `ignore_errors` is true, + then a failure while reporting a single file will not stop the entire + report. + + `file` is a file-like object, suitable for writing. + `include` is a list of file name patterns. Files that match will be 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. + If `skip_covered` is true, don't report on files with 100% coverage. + + If `skip_empty` is true, don't report on empty files (those that have + no statements). + + `contexts` is a list of regular expressions. Only data from + :ref:`dynamic contexts ` that match one of those + expressions (using :func:`re.search `) will be + included in the report. + + `precision` is the number of digits to display after the decimal + point for percentages. + + All of the arguments default to the settings read from the + :ref:`configuration file `. Returns a float, the total percentage covered. + .. versionadded:: 4.0 + The `skip_covered` parameter. + + .. versionadded:: 5.0 + The `contexts` and `skip_empty` parameters. + + .. versionadded:: 5.2 + The `precision` parameter. + """ - self.get_data() - self.config.from_args( + with override_config( + self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, show_missing=show_missing, skip_covered=skip_covered, - ) - reporter = SummaryReporter(self, self.config) - return reporter.report(morfs, outfile=file) + report_contexts=contexts, skip_empty=skip_empty, precision=precision, + sort=sort + ): + reporter = SummaryReporter(self) + return reporter.report(morfs, outfile=file) def annotate( self, morfs=None, directory=None, ignore_errors=None, - omit=None, include=None, + omit=None, include=None, contexts=None, ): """Annotate a list of modules. @@ -1058,16 +901,19 @@ def annotate( See :meth:`report` for other arguments. """ - self.get_data() - self.config.from_args( - ignore_errors=ignore_errors, report_omit=omit, report_include=include - ) - reporter = AnnotateReporter(self, self.config) - 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, - skip_covered=None): + with override_config(self, + ignore_errors=ignore_errors, report_omit=omit, + report_include=include, report_contexts=contexts, + ): + reporter = AnnotateReporter(self) + 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, + skip_covered=None, show_contexts=None, contexts=None, + skip_empty=None, precision=None, + ): """Generate an HTML report. The HTML is written to `directory`. The file "index.html" is the @@ -1084,19 +930,25 @@ def html_report(self, morfs=None, directory=None, ignore_errors=None, Returns a float, the total percentage covered. + .. note:: + The HTML report files are generated incrementally based on the + source files and coverage results. If you modify the report files, + the changes will not be considered. You should be careful about + changing the files in the report folder. + """ - self.get_data() - self.config.from_args( + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_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) + skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, + skip_empty=skip_empty, precision=precision, + ): + reporter = HtmlReporter(self) + return reporter.report(morfs) def xml_report( self, morfs=None, outfile=None, ignore_errors=None, - omit=None, include=None, + omit=None, include=None, contexts=None, skip_empty=None, ): """Generate an XML report of coverage results. @@ -1110,40 +962,35 @@ def xml_report( Returns a float, the total percentage covered. """ - self.get_data() - self.config.from_args( + with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, - xml_output=outfile, - ) - file_to_close = None - delete_file = False - if self.config.xml_output: - if self.config.xml_output == '-': - outfile = sys.stdout - else: - # Ensure that the output directory is created; done here - # because this report pre-opens the output file. - # HTMLReport does this using the Report plumbing because - # its task is more complex, being multiple files. - output_dir = os.path.dirname(self.config.xml_output) - if output_dir and not os.path.isdir(output_dir): - os.makedirs(output_dir) - open_kwargs = {} - if env.PY3: - open_kwargs['encoding'] = 'utf8' - outfile = open(self.config.xml_output, "w", **open_kwargs) - file_to_close = outfile - try: - reporter = XmlReporter(self, self.config) - return reporter.report(morfs, outfile=outfile) - except CoverageException: - delete_file = True - raise - finally: - if file_to_close: - file_to_close.close() - if delete_file: - file_be_gone(self.config.xml_output) + xml_output=outfile, report_contexts=contexts, skip_empty=skip_empty, + ): + return render_report(self.config.xml_output, XmlReporter(self), morfs) + + def json_report( + self, morfs=None, outfile=None, ignore_errors=None, + omit=None, include=None, contexts=None, pretty_print=None, + show_contexts=None + ): + """Generate a JSON report of coverage results. + + Each module in `morfs` is included in the report. `outfile` is the + path to write the file to, "-" will write to stdout. + + See :meth:`report` for other arguments. + + Returns a float, the total percentage covered. + + .. versionadded:: 5.0 + + """ + with override_config(self, + ignore_errors=ignore_errors, report_omit=omit, report_include=include, + json_output=outfile, report_contexts=contexts, json_pretty_print=pretty_print, + json_show_contexts=show_contexts + ): + return render_report(self.config.json_output, JsonReporter(self), morfs) def sys_info(self): """Return a list of (key, value) pairs showing internal information.""" @@ -1151,6 +998,7 @@ def sys_info(self): import coverage as covmod self._init() + self._post_init() def plugin_info(plugins): """Make an entry for the sys_info from a list of plug-ins.""" @@ -1165,71 +1013,51 @@ def plugin_info(plugins): info = [ ('version', covmod.__version__), ('coverage', covmod.__file__), - ('cover_paths', self.cover_paths), - ('pylib_paths', self.pylib_paths), - ('tracer', self.collector.tracer_name()), - ('plugins.file_tracers', plugin_info(self.plugins.file_tracers)), - ('plugins.configurers', plugin_info(self.plugins.configurers)), - ('config_files', self.config.attempted_config_files), - ('configs_read', self.config.config_files), - ('data_path', self.data_files.filename), + ('tracer', self._collector.tracer_name() if self._collector else "-none-"), + ('CTracer', 'available' if CTracer else "unavailable"), + ('plugins.file_tracers', plugin_info(self._plugins.file_tracers)), + ('plugins.configurers', plugin_info(self._plugins.configurers)), + ('plugins.context_switchers', plugin_info(self._plugins.context_switchers)), + ('configs_attempted', self.config.attempted_config_files), + ('configs_read', self.config.config_files_read), + ('config_file', self.config.config_file), + ('config_contents', + repr(self.config._config_contents) + if self.config._config_contents + else '-none-' + ), + ('data_file', self._data.data_filename() if self._data is not None else "-none-"), ('python', sys.version.replace('\n', '')), ('platform', platform.platform()), ('implementation', platform.python_implementation()), ('executable', sys.executable), + ('def_encoding', sys.getdefaultencoding()), + ('fs_encoding', sys.getfilesystemencoding()), + ('pid', os.getpid()), ('cwd', os.getcwd()), ('path', sys.path), ('environment', sorted( ("%s = %s" % (k, v)) for k, v in iitems(os.environ) - if k.startswith(("COV", "PY")) + if any(slug in k for slug in ("COV", "PY")) )), - ('command_line', " ".join(getattr(sys, 'argv', ['???']))), + ('command_line', " ".join(getattr(sys, 'argv', ['-none-']))), ] - matcher_names = [ - 'source_match', 'source_pkgs_match', - 'include_match', 'omit_match', - 'cover_match', 'pylib_match', - ] + if self._inorout: + info.extend(self._inorout.sys_info()) - for matcher_name in matcher_names: - matcher = getattr(self, matcher_name) - if matcher: - matcher_info = matcher.info() - else: - matcher_info = '-none-' - info.append((matcher_name, matcher_info)) + info.extend(CoverageData.sys_info()) return info -# FileDisposition "methods": FileDisposition is a pure value object, so it can -# be implemented in either C or Python. Acting on them is done with these -# functions. - -def _disposition_init(cls, original_filename): - """Construct and initialize a new FileDisposition object.""" - disp = cls() - disp.original_filename = original_filename - disp.canonical_filename = original_filename - disp.source_filename = None - disp.trace = False - disp.reason = "" - disp.file_tracer = None - disp.has_dynamic_filename = False - return disp +# Mega debugging... +# $set_env.py: COVERAGE_DEBUG_CALLS - Lots and lots of output about calls to Coverage. +if int(os.environ.get("COVERAGE_DEBUG_CALLS", 0)): # pragma: debugging + from coverage.debug import decorate_methods, show_calls - -def _disposition_debug_msg(disp): - """Make a nice debug message of what the FileDisposition is doing.""" - if disp.trace: - msg = "Tracing %r" % (disp.original_filename,) - if disp.file_tracer: - msg += ": will be traced by %r" % disp.file_tracer - else: - msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) - return msg + Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) def process_startup(): @@ -1277,10 +1105,11 @@ def process_startup(): cov = Coverage(config_file=cps) process_startup.coverage = cov - cov.start() cov._warn_no_data = False cov._warn_unimported_source = False + cov._warn_preimported_source = False cov._auto_save = True + cov.start() return cov diff --git a/coverage/ctracer/datastack.c b/coverage/ctracer/datastack.c index 515ba9249..a9cfcc2cf 100644 --- a/coverage/ctracer/datastack.c +++ b/coverage/ctracer/datastack.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #include "util.h" #include "datastack.h" diff --git a/coverage/ctracer/datastack.h b/coverage/ctracer/datastack.h index b2dbeb95e..3b3078ba2 100644 --- a/coverage/ctracer/datastack.h +++ b/coverage/ctracer/datastack.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_DATASTACK_H #define _COVERAGE_DATASTACK_H diff --git a/coverage/ctracer/filedisp.c b/coverage/ctracer/filedisp.c index 479a2c9f7..47782ae09 100644 --- a/coverage/ctracer/filedisp.c +++ b/coverage/ctracer/filedisp.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #include "util.h" #include "filedisp.h" diff --git a/coverage/ctracer/filedisp.h b/coverage/ctracer/filedisp.h index ada68eafe..860f9a50b 100644 --- a/coverage/ctracer/filedisp.h +++ b/coverage/ctracer/filedisp.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_FILEDISP_H #define _COVERAGE_FILEDISP_H diff --git a/coverage/ctracer/module.c b/coverage/ctracer/module.c index 762318593..f308902b6 100644 --- a/coverage/ctracer/module.c +++ b/coverage/ctracer/module.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #include "util.h" #include "tracer.h" diff --git a/coverage/ctracer/stats.h b/coverage/ctracer/stats.h index c5ffdf5f2..05173369f 100644 --- a/coverage/ctracer/stats.h +++ b/coverage/ctracer/stats.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_STATS_H #define _COVERAGE_STATS_H diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 6dcdc576d..7d639112d 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ /* C-based Tracer for coverage.py. */ @@ -341,7 +341,6 @@ 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) { @@ -353,7 +352,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) 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) { + if (self->should_start_context != Py_None && self->context == Py_None) { PyObject * context; /* We're looking for our context, ask should_start_context if this is the start. */ STATS( self->stats.start_context_calls++; ) @@ -866,6 +865,8 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse goto error; } + self->activity = TRUE; + switch (what) { case PyTrace_CALL: if (CTracer_handle_call(self, frame) < 0) { diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h index d5d630fb4..a83742ddf 100644 --- a/coverage/ctracer/tracer.h +++ b/coverage/ctracer/tracer.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_TRACER_H #define _COVERAGE_TRACER_H @@ -27,7 +27,6 @@ typedef struct CTracer { PyObject * trace_arcs; PyObject * should_start_context; PyObject * switch_context; - PyObject * context; /* Has the tracer been started? */ BOOL started; @@ -35,6 +34,8 @@ typedef struct CTracer { BOOL tracing_arcs; /* Have we had any activity? */ BOOL activity; + /* The current dynamic context. */ + PyObject * context; /* The data stack is a stack of dictionaries. Each dictionary collects diff --git a/coverage/ctracer/util.h b/coverage/ctracer/util.h index f0c302cfd..5cba9b309 100644 --- a/coverage/ctracer/util.h +++ b/coverage/ctracer/util.h @@ -1,5 +1,5 @@ /* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ #ifndef _COVERAGE_UTIL_H #define _COVERAGE_UTIL_H @@ -44,7 +44,7 @@ #endif /* Py3k */ -// Undocumented, and not in 2.6, so our own copy of it. +// Undocumented, and not in all 2.7.x, so our own copy of it. #define My_XSETREF(op, op2) \ do { \ PyObject *_py_tmp = (PyObject *)(op); \ diff --git a/coverage/data.py b/coverage/data.py index 6f76a727b..82bf1d41c 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -1,794 +1,124 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Coverage data for coverage.py.""" +"""Coverage data for coverage.py. -import glob -import itertools -import json -import optparse -import os -import os.path -import random -import re -import socket - -from coverage import env -from coverage.backward import iitems, string_class -from coverage.debug import _TEST_NAME_FILE -from coverage.files import PathAliases -from coverage.misc import CoverageException, file_be_gone, isolate_module - -os = isolate_module(os) - - -class CoverageData(object): - """Manages collected coverage data, including file storage. - - This class is the public supported API to the data coverage.py collects - during program execution. It includes information about what code was - executed. It does not include information from the analysis phase, to - determine what lines could have been executed, or what lines were not - executed. - - .. note:: - - The file format is not documented or guaranteed. It will change in - the future, in possibly complicated ways. Do not read coverage.py - data files directly. Use this API to avoid disruption. - - There are a number of kinds of data that can be collected: +This file had the 4.x JSON data support, which is now gone. This file still +has storage-agnostic helpers, and is kept to avoid changing too many imports. +CoverageData is now defined in sqldata.py, and imported here to keep the +imports working. - * **lines**: the line numbers of source lines that were executed. - These are always available. +""" - * **arcs**: pairs of source and destination line numbers for transitions - between source lines. These are only available if branch coverage was - used. - - * **file tracer names**: the module names of the file tracer plugins that - handled each file in the data. - - * **run information**: information about the program execution. This is - written during "coverage run", and then accumulated during "coverage - combine". - - Lines, arcs, and file tracer names are stored for each source file. File - names in this API are case-sensitive, even on platforms with - case-insensitive file systems. - - To read a coverage.py data file, use :meth:`read_file`, or - :meth:`read_fileobj` if you have an already-opened file. You can then - access the line, arc, or file tracer data with :meth:`lines`, :meth:`arcs`, - or :meth:`file_tracer`. Run information is available with - :meth:`run_infos`. - - The :meth:`has_arcs` method indicates whether arc data is available. You - can get a list of the files in the data with :meth:`measured_files`. - A summary of the line data is available from :meth:`line_counts`. As with - most Python containers, you can determine if there is any data at all by - using this object as a boolean value. +import glob +import os.path +from coverage.misc import CoverageException, file_be_gone +from coverage.sqldata import CoverageData - Most data files will be created by coverage.py itself, but you can use - methods here to create data files if you like. The :meth:`add_lines`, - :meth:`add_arcs`, and :meth:`add_file_tracers` methods add data, in ways - that are convenient for coverage.py. The :meth:`add_run_info` method adds - key-value pairs to the run information. - To add a file without any measured data, use :meth:`touch_file`. +def line_counts(data, fullpath=False): + """Return a dict summarizing the line coverage data. - You write to a named file with :meth:`write_file`, or to an already opened - file with :meth:`write_fileobj`. + Keys are based on the file names, and values are the number of executed + lines. If `fullpath` is true, then the keys are the full pathnames of + the files, otherwise they are the basenames of the files. - You can clear the data in memory with :meth:`erase`. Two data collections - can be combined by using :meth:`update` on one :class:`CoverageData`, - passing it the other. + Returns a dict mapping file names to counts of lines. """ + summ = {} + if fullpath: + filename_fn = lambda f: f + else: + filename_fn = os.path.basename + for filename in data.measured_files(): + summ[filename_fn(filename)] = len(data.lines(filename)) + return summ - # The data file format is JSON, with these keys: - # - # * lines: a dict mapping file names to lists of line numbers - # executed:: - # - # { "file1": [17,23,45], "file2": [1,2,3], ... } - # - # * arcs: a dict mapping file names to lists of line number pairs:: - # - # { "file1": [[17,23], [17,25], [25,26]], ... } - # - # * file_tracers: a dict mapping file names to plugin names:: - # - # { "file1": "django.coverage", ... } - # - # * runs: a list of dicts of information about the coverage.py runs - # contributing to the data:: - # - # [ { "brief_sys": "CPython 2.7.10 Darwin" }, ... ] - # - # Only one of `lines` or `arcs` will be present: with branch coverage, data - # is stored as arcs. Without branch coverage, it is stored as lines. The - # line data is easily recovered from the arcs: it is all the first elements - # of the pairs that are greater than zero. - - def __init__(self, debug=None): - """Create a CoverageData. - - `debug` is a `DebugControl` object for writing debug messages. - - """ - self._debug = debug - # A map from canonical Python source file name to a dictionary in - # which there's an entry for each line number that has been - # executed: - # - # { 'filename1.py': [12, 47, 1001], ... } - # - self._lines = None +def add_data_to_hash(data, filename, hasher): + """Contribute `filename`'s data to the `hasher`. - # A map from canonical Python source file name to a dictionary with an - # entry for each pair of line numbers forming an arc: - # - # { 'filename1.py': [(12,14), (47,48), ... ], ... } - # - self._arcs = None + `hasher` is a `coverage.misc.Hasher` instance to be updated with + the file's data. It should only get the results data, not the run + data. - # A map from canonical source file name to a plugin module name: - # - # { 'filename1.py': 'django.coverage', ... } - # - self._file_tracers = {} - - # A list of dicts of information about the coverage.py runs. - self._runs = [] - - def __repr__(self): - return "<{klass} lines={lines} arcs={arcs} tracers={tracers} runs={runs}>".format( - klass=self.__class__.__name__, - lines="None" if self._lines is None else "{{{0}}}".format(len(self._lines)), - arcs="None" if self._arcs is None else "{{{0}}}".format(len(self._arcs)), - tracers="{{{0}}}".format(len(self._file_tracers)), - runs="[{0}]".format(len(self._runs)), - ) - - ## - ## Reading data - ## - - def has_arcs(self): - """Does this data have arcs? - - Arc data is only available if branch coverage was used during - collection. - - Returns a boolean. - - """ - return self._has_arcs() - - def lines(self, filename): - """Get the list of lines executed for a file. - - If the file was not measured, returns None. A file might be measured, - and have no lines executed, in which case an empty list is returned. - - If the file was executed, returns a list of integers, the line numbers - executed in the file. The list is in no particular order. - - """ - if self._arcs is not None: - arcs = self._arcs.get(filename) - if arcs is not None: - all_lines = itertools.chain.from_iterable(arcs) - return list(set(l for l in all_lines if l > 0)) - elif self._lines is not None: - return self._lines.get(filename) - return None - - def arcs(self, filename): - """Get the list of arcs executed for a file. - - If the file was not measured, returns None. A file might be measured, - and have no arcs executed, in which case an empty list is returned. - - If the file was executed, returns a list of 2-tuples of integers. Each - pair is a starting line number and an ending line number for a - transition from one line to another. The list is in no particular - order. - - Negative numbers have special meaning. If the starting line number is - -N, it represents an entry to the code object that starts at line N. - If the ending ling number is -N, it's an exit from the code object that - starts at line N. - - """ - if self._arcs is not None: - if filename in self._arcs: - return self._arcs[filename] - return None - - def file_tracer(self, filename): - """Get the plugin name of the file tracer for a file. - - Returns the name of the plugin that handles this file. If the file was - measured, but didn't use a plugin, then "" is returned. If the file - was not measured, then None is returned. + """ + if data.has_arcs(): + hasher.update(sorted(data.arcs(filename) or [])) + else: + hasher.update(sorted(data.lines(filename) or [])) + hasher.update(data.file_tracer(filename)) - """ - # Because the vast majority of files involve no plugin, we don't store - # them explicitly in self._file_tracers. Check the measured data - # instead to see if it was a known file with no plugin. - if filename in (self._arcs or self._lines or {}): - return self._file_tracers.get(filename, "") - return None - def run_infos(self): - """Return the list of dicts of run information. +def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): + """Combine a number of data files together. - For data collected during a single run, this will be a one-element - list. If data has been combined, there will be one element for each - original data file. + Treat `data.filename` as a file prefix, and combine the data from all + of the data files starting with that prefix plus a dot. - """ - return self._runs + If `aliases` is provided, it's a `PathAliases` object that is used to + re-map paths to match the local machine's. - def measured_files(self): - """A list of all files that had been measured.""" - return list(self._arcs or self._lines or {}) + If `data_paths` is provided, it is a list of directories or files to + combine. Directories are searched for files that start with + `data.filename` plus dot as a prefix, and those files are combined. - def line_counts(self, fullpath=False): - """Return a dict summarizing the line coverage data. + If `data_paths` is not provided, then the directory portion of + `data.filename` is used as the directory to search for data files. - Keys are based on the file names, and values are the number of executed - lines. If `fullpath` is true, then the keys are the full pathnames of - the files, otherwise they are the basenames of the files. + Every data file found and combined is then deleted from disk. If a file + cannot be read, a warning will be issued, and the file will not be + deleted. - Returns a dict mapping file names to counts of lines. + If `strict` is true, and no files are found to combine, an error is + raised. - """ - summ = {} - if fullpath: - filename_fn = lambda f: f + """ + # Because of the os.path.abspath in the constructor, data_dir will + # never be an empty string. + data_dir, local = os.path.split(data.base_filename()) + localdot = local + '.*' + + data_paths = data_paths or [data_dir] + files_to_combine = [] + for p in data_paths: + if os.path.isfile(p): + files_to_combine.append(os.path.abspath(p)) + elif os.path.isdir(p): + pattern = os.path.join(os.path.abspath(p), localdot) + files_to_combine.extend(glob.glob(pattern)) else: - filename_fn = os.path.basename - for filename in self.measured_files(): - summ[filename_fn(filename)] = len(self.lines(filename)) - return summ - - def __nonzero__(self): - return bool(self._lines or self._arcs) - - __bool__ = __nonzero__ - - def read_fileobj(self, file_obj): - """Read the coverage data from the given file object. - - Should only be used on an empty CoverageData object. - - """ - data = self._read_raw_data(file_obj) - - self._lines = self._arcs = None - - if 'lines' in data: - self._lines = data['lines'] - if 'arcs' in data: - self._arcs = dict( - (fname, [tuple(pair) for pair in arcs]) - for fname, arcs in iitems(data['arcs']) - ) - self._file_tracers = data.get('file_tracers', {}) - self._runs = data.get('runs', []) - - self._validate() - - def read_file(self, filename): - """Read the coverage data from `filename` into this object.""" - if self._debug and self._debug.should('dataio'): - self._debug.write("Reading data from %r" % (filename,)) + raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) + + if strict and not files_to_combine: + raise CoverageException("No data to combine") + + files_combined = 0 + for f in files_to_combine: + if f == data.data_filename(): + # Sometimes we are combining into a file which is one of the + # parallel files. Skip that file. + if data._debug.should('dataio'): + data._debug.write("Skipping combining ourself: %r" % (f,)) + continue + if data._debug.should('dataio'): + data._debug.write("Combining data file %r" % (f,)) try: - with self._open_for_reading(filename) as f: - self.read_fileobj(f) - except Exception as exc: - raise CoverageException( - "Couldn't read data from '%s': %s: %s" % ( - filename, exc.__class__.__name__, exc, - ) - ) - - _GO_AWAY = "!coverage.py: This is a private format, don't read it directly!" - - @classmethod - def _open_for_reading(cls, filename): - """Open a file appropriately for reading data.""" - return open(filename, "r") - - @classmethod - def _read_raw_data(cls, file_obj): - """Read the raw data from a file object.""" - go_away = file_obj.read(len(cls._GO_AWAY)) - if go_away != cls._GO_AWAY: - raise CoverageException("Doesn't seem to be a coverage.py data file") - return json.load(file_obj) - - @classmethod - def _read_raw_data_file(cls, filename): - """Read the raw data from a file, for debugging.""" - with cls._open_for_reading(filename) as f: - return cls._read_raw_data(f) - - ## - ## Writing data - ## - - def add_lines(self, line_data): - """Add measured line data. - - `line_data` is a dictionary mapping file names to dictionaries:: - - { filename: { lineno: None, ... }, ...} - - """ - if self._debug and self._debug.should('dataop'): - self._debug.write("Adding lines: %d files, %d lines total" % ( - len(line_data), sum(len(lines) for lines in line_data.values()) - )) - if self._has_arcs(): - raise CoverageException("Can't add lines to existing arc data") - - if self._lines is None: - self._lines = {} - for filename, linenos in iitems(line_data): - if filename in self._lines: - new_linenos = set(self._lines[filename]) - new_linenos.update(linenos) - linenos = new_linenos - self._lines[filename] = list(linenos) - - self._validate() - - def add_arcs(self, arc_data): - """Add measured arc data. - - `arc_data` is a dictionary mapping file names to dictionaries:: - - { filename: { (l1,l2): None, ... }, ...} - - """ - if self._debug and self._debug.should('dataop'): - self._debug.write("Adding arcs: %d files, %d arcs total" % ( - len(arc_data), sum(len(arcs) for arcs in arc_data.values()) - )) - if self._has_lines(): - raise CoverageException("Can't add arcs to existing line data") - - if self._arcs is None: - self._arcs = {} - for filename, arcs in iitems(arc_data): - if filename in self._arcs: - new_arcs = set(self._arcs[filename]) - new_arcs.update(arcs) - arcs = new_arcs - self._arcs[filename] = list(arcs) - - self._validate() - - def add_file_tracers(self, file_tracers): - """Add per-file plugin information. - - `file_tracers` is { filename: plugin_name, ... } - - """ - if self._debug and self._debug.should('dataop'): - self._debug.write("Adding file tracers: %d files" % (len(file_tracers),)) - - existing_files = self._arcs or self._lines or {} - for filename, plugin_name in iitems(file_tracers): - if filename not in existing_files: - raise CoverageException( - "Can't add file tracer data for unmeasured file '%s'" % (filename,) - ) - existing_plugin = self._file_tracers.get(filename) - if existing_plugin is not None and plugin_name != existing_plugin: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, existing_plugin, plugin_name, - ) - ) - self._file_tracers[filename] = plugin_name - - self._validate() - - def add_run_info(self, **kwargs): - """Add information about the run. - - Keywords are arbitrary, and are stored in the run dictionary. Values - must be JSON serializable. You may use this function more than once, - but repeated keywords overwrite each other. - - """ - if self._debug and self._debug.should('dataop'): - self._debug.write("Adding run info: %r" % (kwargs,)) - if not self._runs: - self._runs = [{}] - self._runs[0].update(kwargs) - self._validate() - - 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(): - raise CoverageException("Can't touch files in an empty CoverageData") - - if self._has_arcs(): - where = self._arcs + new_data = CoverageData(f, debug=data._debug) + new_data.read() + except CoverageException as exc: + if data._warn: + # The CoverageException has the file name in it, so just + # use the message as the warning. + data._warn(str(exc)) else: - where = self._lines - where.setdefault(filename, []) - if plugin_name: - # Set the tracer for this file - self._file_tracers[filename] = plugin_name - - self._validate() - - def write_fileobj(self, file_obj): - """Write the coverage data to `file_obj`.""" - - # Create the file data. - file_data = {} - - if self._has_arcs(): - file_data['arcs'] = self._arcs - - if self._has_lines(): - file_data['lines'] = self._lines - - if self._file_tracers: - file_data['file_tracers'] = self._file_tracers - - if self._runs: - file_data['runs'] = self._runs - - # Write the data to the file. - file_obj.write(self._GO_AWAY) - json.dump(file_data, file_obj, separators=(',', ':')) - - def write_file(self, filename): - """Write the coverage data to `filename`.""" - if self._debug and self._debug.should('dataio'): - self._debug.write("Writing data to %r" % (filename,)) - with open(filename, 'w') as fdata: - self.write_fileobj(fdata) - - def erase(self): - """Erase the data in this object.""" - self._lines = None - self._arcs = None - self._file_tracers = {} - self._runs = [] - self._validate() - - def update(self, other_data, aliases=None): - """Update this data with data from another `CoverageData`. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - """ - if self._has_lines() and other_data._has_arcs(): - raise CoverageException("Can't combine arc data with line data") - if self._has_arcs() and other_data._has_lines(): - raise CoverageException("Can't combine line data with arc data") - - aliases = aliases or PathAliases() - - # _file_tracers: only have a string, so they have to agree. - # Have to do these first, so that our examination of self._arcs and - # self._lines won't be confused by data updated from other_data. - for filename in other_data.measured_files(): - other_plugin = other_data.file_tracer(filename) - filename = aliases.map(filename) - this_plugin = self.file_tracer(filename) - if this_plugin is None: - if other_plugin: - self._file_tracers[filename] = other_plugin - elif this_plugin != other_plugin: - raise CoverageException( - "Conflicting file tracer name for '%s': %r vs %r" % ( - filename, this_plugin, other_plugin, - ) - ) - - # _runs: add the new runs to these runs. - self._runs.extend(other_data._runs) - - # _lines: merge dicts. - if other_data._has_lines(): - if self._lines is None: - self._lines = {} - for filename, file_lines in iitems(other_data._lines): - filename = aliases.map(filename) - if filename in self._lines: - lines = set(self._lines[filename]) - lines.update(file_lines) - file_lines = list(lines) - self._lines[filename] = file_lines - - # _arcs: merge dicts. - if other_data._has_arcs(): - if self._arcs is None: - self._arcs = {} - for filename, file_arcs in iitems(other_data._arcs): - filename = aliases.map(filename) - if filename in self._arcs: - arcs = set(self._arcs[filename]) - arcs.update(file_arcs) - file_arcs = list(arcs) - self._arcs[filename] = file_arcs - - self._validate() - - ## - ## Miscellaneous - ## - - def _validate(self): - """If we are in paranoid mode, validate that everything is right.""" - if env.TESTING: - self._validate_invariants() - - def _validate_invariants(self): - """Validate internal invariants.""" - # Only one of _lines or _arcs should exist. - assert not(self._has_lines() and self._has_arcs()), ( - "Shouldn't have both _lines and _arcs" - ) - - # _lines should be a dict of lists of ints. - if self._has_lines(): - for fname, lines in iitems(self._lines): - assert isinstance(fname, string_class), "Key in _lines shouldn't be %r" % (fname,) - assert all(isinstance(x, int) for x in lines), ( - "_lines[%r] shouldn't be %r" % (fname, lines) - ) - - # _arcs should be a dict of lists of pairs of ints. - if self._has_arcs(): - for fname, arcs in iitems(self._arcs): - assert isinstance(fname, string_class), "Key in _arcs shouldn't be %r" % (fname,) - assert all(isinstance(x, int) and isinstance(y, int) for x, y in arcs), ( - "_arcs[%r] shouldn't be %r" % (fname, arcs) - ) - - # _file_tracers should have only non-empty strings as values. - for fname, plugin in iitems(self._file_tracers): - assert isinstance(fname, string_class), ( - "Key in _file_tracers shouldn't be %r" % (fname,) - ) - assert plugin and isinstance(plugin, string_class), ( - "_file_tracers[%r] shoudn't be %r" % (fname, plugin) - ) - - # _runs should be a list of dicts. - for val in self._runs: - assert isinstance(val, dict) - for key in val: - assert isinstance(key, string_class), "Key in _runs shouldn't be %r" % (key,) - - def add_to_hash(self, filename, hasher): - """Contribute `filename`'s data to the `hasher`. - - `hasher` is a `coverage.misc.Hasher` instance to be updated with - the file's data. It should only get the results data, not the run - data. - - """ - if self._has_arcs(): - hasher.update(sorted(self.arcs(filename) or [])) - else: - hasher.update(sorted(self.lines(filename) or [])) - hasher.update(self.file_tracer(filename)) - - ## - ## Internal - ## - - def _has_lines(self): - """Do we have data in self._lines?""" - return self._lines is not None - - def _has_arcs(self): - """Do we have data in self._arcs?""" - return self._arcs is not None - - -class CoverageDataFiles(object): - """Manage the use of coverage data files.""" - - 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") - - def erase(self, parallel=False): - """Erase the data from the file storage. - - If `parallel` is true, then also deletes data files created from the - 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): - """Read the coverage data.""" - if os.path.exists(self.filename): - data.read_file(self.filename) - - def write(self, data, suffix=None): - """Write the collected coverage data to a file. - - `suffix` is a suffix to append to the base file name. This can be used - for multiple or parallel execution, so that many coverage data files - can exist simultaneously. A dot will be used to join the base name and - the suffix. - - """ - filename = self.filename - if suffix is True: - # If data_suffix was a simple true value, then make a suffix with - # plenty of distinguishing information. We do this here in - # `save()` at the last minute so that the pid will be correct even - # if the process forks. - extra = "" - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE) as f: - test_name = f.read() - extra = "." + test_name - dice = random.Random(os.urandom(8)).randint(0, 999999) - suffix = "%s%s.%s.%06d" % (socket.gethostname(), extra, os.getpid(), dice) - - if suffix: - filename += "." + suffix - data.write_file(filename) - - def combine_parallel_data(self, data, aliases=None, data_paths=None, strict=False): - """Combine a number of data files together. - - Treat `self.filename` as a file prefix, and combine the data from all - of the data files starting with that prefix plus a dot. - - If `aliases` is provided, it's a `PathAliases` object that is used to - re-map paths to match the local machine's. - - If `data_paths` is provided, it is a list of directories or files to - combine. Directories are searched for files that start with - `self.filename` plus dot as a prefix, and those files are combined. - - If `data_paths` is not provided, then the directory portion of - `self.filename` is used as the directory to search for data files. - - Every data file found and combined is then deleted from disk. If a file - cannot be read, a warning will be issued, and the file will not be - deleted. - - If `strict` is true, and no files are found to combine, an error is - raised. - - """ - # Because of the os.path.abspath in the constructor, data_dir will - # never be an empty string. - data_dir, local = os.path.split(self.filename) - localdot = local + '.*' - - data_paths = data_paths or [data_dir] - files_to_combine = [] - for p in data_paths: - if os.path.isfile(p): - files_to_combine.append(os.path.abspath(p)) - elif os.path.isdir(p): - pattern = os.path.join(os.path.abspath(p), localdot) - files_to_combine.extend(glob.glob(pattern)) - else: - raise CoverageException("Couldn't combine from non-existent path '%s'" % (p,)) - - if strict and not files_to_combine: - raise CoverageException("No data to combine") - - files_combined = 0 - for f in files_to_combine: - new_data = CoverageData(debug=self.debug) - try: - new_data.read_file(f) - except CoverageException as exc: - if self.warn: - # The CoverageException has the file name in it, so just - # use the message as the warning. - self.warn(str(exc)) - else: - data.update(new_data, aliases=aliases) - files_combined += 1 - if self.debug and self.debug.should('dataio'): - self.debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) - - if strict and not files_combined: - raise CoverageException("No usable data files") - - -def canonicalize_json_data(data): - """Canonicalize our JSON data so it can be compared.""" - for fname, lines in iitems(data.get('lines', {})): - data['lines'][fname] = sorted(lines) - for fname, arcs in iitems(data.get('arcs', {})): - data['arcs'][fname] = sorted(arcs) - - -def pretty_data(data): - """Format data as JSON, but as nicely as possible. - - Returns a string. - - """ - # Start with a basic JSON dump. - out = json.dumps(data, indent=4, sort_keys=True) - # But pairs of numbers shouldn't be split across lines... - out = re.sub(r"\[\s+(-?\d+),\s+(-?\d+)\s+]", r"[\1, \2]", out) - # Trailing spaces mess with tests, get rid of them. - out = re.sub(r"(?m)\s+$", "", out) - return out - - -def debug_main(args): - """Dump the raw data from data files. - - Run this as:: - - $ python -m coverage.data [FILE] - - """ - parser = optparse.OptionParser() - parser.add_option( - "-c", "--canonical", action="store_true", - help="Sort data into a canonical order", - ) - options, args = parser.parse_args(args) - - for filename in (args or [".coverage"]): - print("--- {0} ------------------------------".format(filename)) - data = CoverageData._read_raw_data_file(filename) - if options.canonical: - canonicalize_json_data(data) - print(pretty_data(data)) - - -if __name__ == '__main__': - import sys - debug_main(sys.argv[1:]) + data.update(new_data, aliases=aliases) + files_combined += 1 + if data._debug.should('dataio'): + data._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) + + if strict and not files_combined: + raise CoverageException("No usable data files") diff --git a/coverage/debug.py b/coverage/debug.py index e68736f6e..194f16f50 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -1,19 +1,21 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Control of and utilities for debugging.""" import contextlib +import functools import inspect +import itertools import os -import re +import pprint import sys try: import _thread except ImportError: import thread as _thread -from coverage.backward import StringIO +from coverage.backward import reprlib, StringIO from coverage.misc import isolate_module os = isolate_module(os) @@ -23,28 +25,28 @@ # debugging the configuration mechanisms you usually use to control debugging! # This is a list of forced debugging options. FORCED_DEBUG = [] - -# A hack for debugging testing in sub-processes. -_TEST_NAME_FILE = "" # "/tmp/covtest.txt" +FORCED_DEBUG_FILE = None class DebugControl(object): """Control and output for debugging.""" + show_repr_attr = False # For SimpleReprMixin + def __init__(self, options, output): """Configure the options and output file for debugging.""" 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, + self.output = DebugOutputFile.get_one( + output, show_process=self.should('process'), filters=filters, ) + self.raw_output = self.output.outfile def __repr__(self): return "" % (self.options, self.raw_output) @@ -72,6 +74,10 @@ def write(self, msg): """ self.output.write(msg+"\n") + if self.should('self'): + caller_self = inspect.stack()[1][0].f_locals.get('self') + if caller_self is not None: + self.output.write("self: {!r}\n".format(caller_self)) if self.should('callers'): dump_stack_frames(out=self.output, skip=1) self.output.flush() @@ -87,9 +93,16 @@ def get_output(self): return self.raw_output.getvalue() +class NoDebugging(object): + """A replacement for DebugControl that will never try to do anything.""" + def should(self, option): # pylint: disable=unused-argument + """Should we write debug messages? Never.""" + return False + + def info_header(label): """Make a nice header string.""" - return "--{0:-<60s}".format(" "+label+" ") + return "--{:-<60s}".format(" "+label+" ") def info_formatter(info): @@ -102,7 +115,8 @@ def info_formatter(info): info = list(info) if not info: return - label_len = max(len(l) for l, _d in info) + label_len = 30 + assert all(len(l) < label_len for l, _ in info) for label, data in info: if data == []: data = "-none-" @@ -141,7 +155,7 @@ def short_stack(limit=None, skip=0): """ stack = inspect.stack()[limit:skip:-1] - return "\n".join("%30s : %s @%d" % (t[3], t[1], t[2]) for t in stack) + return "\n".join("%30s : %s:%d" % (t[3], t[1], t[2]) for t in stack) def dump_stack_frames(limit=None, out=None, skip=0): @@ -151,6 +165,13 @@ def dump_stack_frames(limit=None, out=None, skip=0): out.write("\n") +def clipped_repr(text, numchars=50): + """`repr(text)`, but limited to `numchars`.""" + r = reprlib.Repr() + r.maxstring = numchars + return r.repr(text) + + def short_id(id64): """Given a 64-bit id, make a shorter 16-bit one.""" id16 = 0 @@ -162,11 +183,47 @@ def short_id(id64): 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) + tid = "{:04x}".format(short_id(_thread.get_ident())) + text = "{:5d}.{}: {}".format(os.getpid(), tid, text) return text +class SimpleReprMixin(object): + """A mixin implementing a simple __repr__.""" + simple_repr_ignore = ['simple_repr_ignore', '$coverage.object_id'] + + def __repr__(self): + show_attrs = ( + (k, v) for k, v in self.__dict__.items() + if getattr(v, "show_repr_attr", True) + and not callable(v) + and k not in self.simple_repr_ignore + ) + return "<{klass} @0x{id:x} {attrs}>".format( + klass=self.__class__.__name__, + id=id(self), + attrs=" ".join("{}={!r}".format(k, v) for k, v in show_attrs), + ) + + +def simplify(v): # pragma: debugging + """Turn things which are nearly dict/list/etc into dict/list/etc.""" + if isinstance(v, dict): + return {k:simplify(vv) for k, vv in v.items()} + elif isinstance(v, (list, tuple)): + return type(v)(simplify(vv) for vv in v) + elif hasattr(v, "__dict__"): + return simplify({'.'+k: v for k, v in v.__dict__.items()}) + else: + return v + + +def pp(v): # pragma: debugging + """Debug helper to pretty-print data, including SimpleNamespace objects.""" + # Might not be needed in 3.9+ + pprint.pprint(simplify(v)) + + def filter_text(text, filters): """Run `text` through a series of filters. @@ -197,7 +254,7 @@ 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 + text = "cwd is now {!r}\n".format(cwd) + text self.cwd = cwd return text @@ -210,32 +267,51 @@ def __init__(self, outfile, show_process, filters): 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,)) + self.filters.insert(0, CwdTracker().filter) + self.write("New process: executable: %r\n" % (sys.executable,)) + self.write("New process: cmd: %r\n" % (getattr(sys, 'argv', None),)) if hasattr(os, 'getppid'): - self.write("New process: parent pid: %s\n" % (os.getppid(),)) + self.write("New process: pid: %r, parent pid: %r\n" % (os.getpid(), 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. + def get_one(cls, fileobj=None, show_process=True, filters=(), interim=False): + """Get a 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. + If `fileobj` is provided, then a new DebugOutputFile is made with it. + + If `fileobj` isn't provided, then a file is chosen + (COVERAGE_DEBUG_FILE, or stderr), and a process-wide singleton + DebugOutputFile is made. + + `show_process` controls whether the debug file adds process-level + information, and filters is a list of other message filters to apply. + + `filters` are the text filters to apply to the stream to annotate with + pids, etc. + + If `interim` is true, then a future `get_one` can replace this one. """ + if fileobj is not None: + # Make DebugOutputFile around the fileobj passed. + return cls(fileobj, show_process, filters) + # 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) + the_one, is_interim = sys.modules.get(cls.SYS_MOD_NAME, (None, True)) + if the_one is None or is_interim: + if fileobj is None: + debug_file_name = os.environ.get("COVERAGE_DEBUG_FILE", FORCED_DEBUG_FILE) + if debug_file_name: + fileobj = open(debug_file_name, "a") + else: + fileobj = sys.stderr + the_one = cls(fileobj, show_process, filters) + sys.modules[cls.SYS_MOD_NAME] = (the_one, interim) return the_one def write(self, text): @@ -250,46 +326,81 @@ def flush(self): def log(msg, stack=False): # pragma: debugging """Write a log message as forcefully as possible.""" - out = DebugOutputFile.the_one() + out = DebugOutputFile.get_one(interim=True) 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 - """For debugging, we can use aspectlib to trace execution. - - Define COVERAGE_ASPECTLIB to enable and configure aspectlib to trace - execution:: - - $ 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. - - """ - aspects = os.environ.get("COVERAGE_ASPECTLIB", "") - if not aspects: - return - - import aspectlib # pylint: disable=import-error - import aspectlib.debug # pylint: disable=import-error - - 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.split(':'): - aspectlib.weave(aspect, aspect_log, methods=public_methods) +def decorate_methods(decorator, butnot=(), private=False): # pragma: debugging + """A class decorator to apply a decorator to methods.""" + def _decorator(cls): + for name, meth in inspect.getmembers(cls, inspect.isroutine): + if name not in cls.__dict__: + continue + if name != "__init__": + if not private and name.startswith("_"): + continue + if name in butnot: + continue + setattr(cls, name, decorator(meth)) + return cls + return _decorator + + +def break_in_pudb(func): # pragma: debugging + """A function decorator to stop in the debugger for each call.""" + @functools.wraps(func) + def _wrapper(*args, **kwargs): + import pudb + sys.stdout = sys.__stdout__ + pudb.set_trace() + return func(*args, **kwargs) + return _wrapper + + +OBJ_IDS = itertools.count() +CALLS = itertools.count() +OBJ_ID_ATTR = "$coverage.object_id" + +def show_calls(show_args=True, show_stack=False, show_return=False): # pragma: debugging + """A method decorator to debug-log each call to the function.""" + def _decorator(func): + @functools.wraps(func) + def _wrapper(self, *args, **kwargs): + oid = getattr(self, OBJ_ID_ATTR, None) + if oid is None: + oid = "{:08d} {:04d}".format(os.getpid(), next(OBJ_IDS)) + setattr(self, OBJ_ID_ATTR, oid) + extra = "" + if show_args: + eargs = ", ".join(map(repr, args)) + ekwargs = ", ".join("{}={!r}".format(*item) for item in kwargs.items()) + extra += "(" + extra += eargs + if eargs and ekwargs: + extra += ", " + extra += ekwargs + extra += ")" + if show_stack: + extra += " @ " + extra += "; ".join(_clean_stack_line(l) for l in short_stack().splitlines()) + callid = next(CALLS) + msg = "{} {:04d} {}{}\n".format(oid, callid, func.__name__, extra) + DebugOutputFile.get_one(interim=True).write(msg) + ret = func(self, *args, **kwargs) + if show_return: + msg = "{} {:04d} {} return {!r}\n".format(oid, callid, func.__name__, ret) + DebugOutputFile.get_one(interim=True).write(msg) + return ret + return _wrapper + return _decorator + + +def _clean_stack_line(s): # pragma: debugging + """Simplify some paths in a stack trace, for compactness.""" + s = s.strip() + s = s.replace(os.path.dirname(__file__) + '/', '') + s = s.replace(os.path.dirname(os.__file__) + '/', '') + s = s.replace(sys.prefix + '/', '') + return s diff --git a/coverage/disposition.py b/coverage/disposition.py new file mode 100644 index 000000000..9b9a997d8 --- /dev/null +++ b/coverage/disposition.py @@ -0,0 +1,37 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Simple value objects for tracking what to do with files.""" + + +class FileDisposition(object): + """A simple value type for recording what to do with a file.""" + pass + + +# FileDisposition "methods": FileDisposition is a pure value object, so it can +# be implemented in either C or Python. Acting on them is done with these +# functions. + +def disposition_init(cls, original_filename): + """Construct and initialize a new FileDisposition object.""" + disp = cls() + disp.original_filename = original_filename + disp.canonical_filename = original_filename + disp.source_filename = None + disp.trace = False + disp.reason = "" + disp.file_tracer = None + disp.has_dynamic_filename = False + return disp + + +def disposition_debug_msg(disp): + """Make a nice debug message of what the FileDisposition is doing.""" + if disp.trace: + msg = "Tracing %r" % (disp.original_filename,) + if disp.file_tracer: + msg += ": will be traced by %r" % disp.file_tracer + else: + msg = "Not tracing %r: %s" % (disp.original_filename, disp.reason) + return msg diff --git a/coverage/env.py b/coverage/env.py index 4699a1e52..b5da3b471 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Determine facts about the environment.""" @@ -9,20 +9,81 @@ # Operating systems. WINDOWS = sys.platform == "win32" -LINUX = sys.platform == "linux2" +LINUX = sys.platform.startswith("linux") + +# Python versions. We amend version_info with one more value, a zero if an +# official version, or 1 if built from source beyond an official version. +PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),) +PY2 = PYVERSION < (3, 0) +PY3 = PYVERSION >= (3, 0) # Python implementations. PYPY = (platform.python_implementation() == 'PyPy') if PYPY: PYPYVERSION = sys.pypy_version_info +PYPY2 = PYPY and PY2 +PYPY3 = PYPY and PY3 + JYTHON = (platform.python_implementation() == 'Jython') IRONPYTHON = (platform.python_implementation() == 'IronPython') -# Python versions. -PYVERSION = sys.version_info -PY2 = PYVERSION < (3, 0) -PY3 = PYVERSION >= (3, 0) +# Python behavior +class PYBEHAVIOR(object): + """Flags indicating this Python's behavior.""" + + # Is "if __debug__" optimized away? + optimize_if_debug = (not PYPY) + + # Is "if not __debug__" optimized away? + optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4)) + + # Is "if not __debug__" optimized away even better? + optimize_if_not_debug2 = (not PYPY) and (PYVERSION >= (3, 8, 0, 'beta', 1)) + + # Do we have yield-from? + yield_from = (PYVERSION >= (3, 3)) + + # Do we have PEP 420 namespace packages? + namespaces_pep420 = (PYVERSION >= (3, 3)) + + # Do .pyc files have the source file size recorded in them? + size_in_pyc = (PYVERSION >= (3, 3)) + + # Do we have async and await syntax? + async_syntax = (PYVERSION >= (3, 5)) + + # PEP 448 defined additional unpacking generalizations + unpackings_pep448 = (PYVERSION >= (3, 5)) + + # Can co_lnotab have negative deltas? + negative_lnotab = (PYVERSION >= (3, 6)) and not (PYPY and PYPYVERSION < (7, 2)) + + # Do .pyc files conform to PEP 552? Hash-based pyc's. + hashed_pyc_pep552 = (PYVERSION >= (3, 7, 0, 'alpha', 4)) + + # Python 3.7.0b3 changed the behavior of the sys.path[0] entry for -m. It + # used to be an empty string (meaning the current directory). It changed + # to be the actual path to the current directory, so that os.chdir wouldn't + # affect the outcome. + actual_syspath0_dash_m = (PYVERSION >= (3, 7, 0, 'beta', 3)) + + # When a break/continue/return statement in a try block jumps to a finally + # block, does the finally block do the break/continue/return (pre-3.8), or + # does the finally jump back to the break/continue/return (3.8) to do the + # work? + finally_jumps_back = (PYVERSION >= (3, 8)) + + # When a function is decorated, does the trace function get called for the + # @-line and also the def-line (new behavior in 3.8)? Or just the @-line + # (old behavior)? + trace_decorated_def = (PYVERSION >= (3, 8)) + + # Are while-true loops optimized into absolute jumps with no loop setup? + nix_while_true = (PYVERSION >= (3, 8)) + + # Python 3.9a1 made sys.argv[0] and other reported files absolute paths. + report_absolute_files = (PYVERSION >= (3, 9)) # Coverage.py specifics. diff --git a/coverage/execfile.py b/coverage/execfile.py index 42e0d96ab..29409d517 100644 --- a/coverage/execfile.py +++ b/coverage/execfile.py @@ -1,16 +1,19 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Execute files of Python code.""" +import inspect import marshal import os import struct import sys import types +from coverage import env from coverage.backward import BUILTINS from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec +from coverage.files import canonical_filename, python_reported_file from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module from coverage.phystokens import compile_unicode from coverage.python import get_python_source @@ -31,8 +34,8 @@ def __init__(self, fullname, *_args): def find_module(modulename): """Find the module named `modulename`. - Returns the file path of the module, and the name of the enclosing - package. + Returns the file path of the module, the name of the enclosing + package, and the spec. """ try: spec = importlib_util_find_spec(modulename) @@ -42,7 +45,7 @@ def find_module(modulename): raise NoSource("No module named %r" % (modulename,)) pathname = spec.origin packagename = spec.name - if pathname.endswith("__init__.py") and not modulename.endswith("__init__"): + if spec.submodule_search_locations: mod_main = modulename + ".__main__" spec = importlib_util_find_spec(mod_main) if not spec: @@ -54,13 +57,13 @@ def find_module(modulename): pathname = spec.origin packagename = spec.name packagename = packagename.rpartition(".")[0] - return pathname, packagename + return pathname, packagename, spec else: def find_module(modulename): """Find the module named `modulename`. - Returns the file path of the module, and the name of the enclosing - package. + Returns the file path of the module, the name of the enclosing + package, and None (where a spec would have been). """ openfile = None glo, loc = globals(), locals() @@ -96,93 +99,153 @@ def find_module(modulename): if openfile: openfile.close() - return pathname, packagename + return pathname, packagename, None -def run_python_module(modulename, args): - """Run a Python module, as though with ``python -m name args...``. +class PyRunner(object): + """Multi-stage execution of Python code. - `modulename` is the name of the module, possibly a dot-separated name. - `args` is the argument array to present as sys.argv, including the first - element naming the module being executed. + This is meant to emulate real Python execution as closely as possible. """ - pathname, packagename = find_module(modulename) + def __init__(self, args, as_module=False): + self.args = args + self.as_module = as_module - pathname = os.path.abspath(pathname) - args[0] = pathname - run_python_file(pathname, args, package=packagename, modulename=modulename, path0="") + self.arg0 = args[0] + self.package = self.modulename = self.pathname = self.loader = self.spec = None + def prepare(self): + """Set sys.path properly. -def run_python_file(filename, args, package=None, modulename=None, path0=None): - """Run a Python file as if it were the main program on the command line. + This needs to happen before any importing, and without importing anything. + """ + if self.as_module: + if env.PYBEHAVIOR.actual_syspath0_dash_m: + path0 = os.getcwd() + else: + path0 = "" + elif os.path.isdir(self.arg0): + # Running a directory means running the __main__.py file in that + # directory. + path0 = self.arg0 + else: + path0 = os.path.abspath(os.path.dirname(self.arg0)) + + if os.path.isdir(sys.path[0]): + # sys.path fakery. If we are being run as a command, then sys.path[0] + # is the directory of the "coverage" script. If this is so, replace + # sys.path[0] with the directory of the file we're running, or the + # current directory when running modules. If it isn't so, then we + # don't know what's going on, and just leave it alone. + top_file = inspect.stack()[-1][0].f_code.co_filename + sys_path_0_abs = os.path.abspath(sys.path[0]) + top_file_dir_abs = os.path.abspath(os.path.dirname(top_file)) + sys_path_0_abs = canonical_filename(sys_path_0_abs) + top_file_dir_abs = canonical_filename(top_file_dir_abs) + if sys_path_0_abs != top_file_dir_abs: + path0 = None - `filename` is the path to the file to execute, it need not be a .py file. - `args` is the argument array to present as sys.argv, including the first - element naming the file being executed. `package` is the name of the - enclosing package, if any. + else: + # sys.path[0] is a file. Is the next entry the directory containing + # that file? + if sys.path[1] == os.path.dirname(sys.path[0]): + # Can it be right to always remove that? + del sys.path[1] - `modulename` is the name of the module the file was run as. + if path0 is not None: + sys.path[0] = python_reported_file(path0) - `path0` is the value to put into sys.path[0]. If it's None, then this - function will decide on a value. + def _prepare2(self): + """Do more preparation to run Python code. - """ - if modulename is None and sys.version_info >= (3, 3): - modulename = '__main__' - - # Create a module to serve as __main__ - old_main_mod = sys.modules['__main__'] - main_mod = types.ModuleType('__main__') - sys.modules['__main__'] = main_mod - main_mod.__file__ = filename - if package: - main_mod.__package__ = package - if modulename: - main_mod.__loader__ = DummyLoader(modulename) - - main_mod.__builtins__ = BUILTINS - - # Set sys.argv properly. - old_argv = sys.argv - sys.argv = args - - if os.path.isdir(filename): - # Running a directory means running the __main__.py file in that - # directory. - my_path0 = filename - - for ext in [".py", ".pyc", ".pyo"]: - try_filename = os.path.join(filename, "__main__" + ext) - if os.path.exists(try_filename): - filename = try_filename - break + Includes finding the module to run and adjusting sys.argv[0]. + This method is allowed to import code. + + """ + if self.as_module: + self.modulename = self.arg0 + pathname, self.package, self.spec = find_module(self.modulename) + if self.spec is not None: + self.modulename = self.spec.name + self.loader = DummyLoader(self.modulename) + self.pathname = os.path.abspath(pathname) + self.args[0] = self.arg0 = self.pathname + elif os.path.isdir(self.arg0): + # Running a directory means running the __main__.py file in that + # directory. + for ext in [".py", ".pyc", ".pyo"]: + try_filename = os.path.join(self.arg0, "__main__" + ext) + if os.path.exists(try_filename): + self.arg0 = try_filename + break + else: + raise NoSource("Can't find '__main__' module in '%s'" % self.arg0) + + if env.PY2: + self.arg0 = os.path.abspath(self.arg0) + + # Make a spec. I don't know if this is the right way to do it. + try: + import importlib.machinery + except ImportError: + pass + else: + try_filename = python_reported_file(try_filename) + self.spec = importlib.machinery.ModuleSpec("__main__", None, origin=try_filename) + self.spec.has_location = True + self.package = "" + self.loader = DummyLoader("__main__") else: - raise NoSource("Can't find '__main__' module in '%s'" % filename) - else: - my_path0 = os.path.abspath(os.path.dirname(filename)) + if env.PY3: + self.loader = DummyLoader("__main__") - # Set sys.path correctly. - old_path0 = sys.path[0] - sys.path[0] = path0 if path0 is not None else my_path0 + self.arg0 = python_reported_file(self.arg0) + + def run(self): + """Run the Python code!""" + + self._prepare2() + + # Create a module to serve as __main__ + main_mod = types.ModuleType('__main__') + + from_pyc = self.arg0.endswith((".pyc", ".pyo")) + main_mod.__file__ = self.arg0 + if from_pyc: + main_mod.__file__ = main_mod.__file__[:-1] + if self.package is not None: + main_mod.__package__ = self.package + main_mod.__loader__ = self.loader + if self.spec is not None: + main_mod.__spec__ = self.spec + + main_mod.__builtins__ = BUILTINS + + sys.modules['__main__'] = main_mod + + # Set sys.argv properly. + sys.argv = self.args - try: try: # Make a code object somehow. - if filename.endswith((".pyc", ".pyo")): - code = make_code_from_pyc(filename) + if from_pyc: + code = make_code_from_pyc(self.arg0) else: - code = make_code_from_py(filename) + code = make_code_from_py(self.arg0) 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)) + msg = "Couldn't run '{filename}' as Python code: {exc.__class__.__name__}: {exc}" + raise CoverageException(msg.format(filename=self.arg0, exc=exc)) # Execute the code object. + # Return to the original directory in case the test code exits in + # a non-existent directory. + cwd = os.getcwd() try: exec(code, main_mod.__dict__) - except SystemExit: + except SystemExit: # pylint: disable=try-except-raise # The user called sys.exit(). Just pass it along to the upper # layers, where it will be handled. raise @@ -205,7 +268,7 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): if hasattr(err, "__traceback__"): err.__traceback__ = err.__traceback__.tb_next sys.excepthook(typ, err, tb.tb_next) - except SystemExit: + except SystemExit: # pylint: disable=try-except-raise raise except Exception: # Getting the output right in the case of excepthook @@ -220,12 +283,37 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None): raise ExceptionDuringRun(typ, err, tb.tb_next) else: sys.exit(1) + finally: + os.chdir(cwd) + + +def run_python_module(args): + """Run a Python module, as though with ``python -m name args...``. + + `args` is the argument array to present as sys.argv, including the first + element naming the module being executed. + + This is a helper for tests, to encapsulate how to use PyRunner. - finally: - # Restore the old __main__, argv, and path. - sys.modules['__main__'] = old_main_mod - sys.argv = old_argv - sys.path[0] = old_path0 + """ + runner = PyRunner(args, as_module=True) + runner.prepare() + runner.run() + + +def run_python_file(args): + """Run a Python file as if it were the main program on the command line. + + `args` is the argument array to present as sys.argv, including the first + element naming the file being executed. `package` is the name of the + enclosing package, if any. + + This is a helper for tests, to encapsulate how to use PyRunner. + + """ + runner = PyRunner(args, as_module=False) + runner.prepare() + runner.run() def make_code_from_py(filename): @@ -252,10 +340,10 @@ def make_code_from_pyc(filename): # match or we won't run the file. magic = fpyc.read(4) if magic != PYC_MAGIC_NUMBER: - raise NoCode("Bad magic number in .pyc file") + raise NoCode("Bad magic number in .pyc file: {} != {}".format(magic, PYC_MAGIC_NUMBER)) date_based = True - if sys.version_info >= (3, 7, 0, 'alpha', 4): + if env.PYBEHAVIOR.hashed_pyc_pep552: flags = struct.unpack('= (3, 3): + if env.PYBEHAVIOR.size_in_pyc: # 3.3 added another long to the header (size), skip it. fpyc.read(4) diff --git a/coverage/files.py b/coverage/files.py index 759ec2c97..5c2ff1ace 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """File wrangling.""" @@ -59,6 +59,7 @@ def canonical_filename(filename): """ if filename not in CANONICAL_FILENAME_CACHE: + cf = filename if not os.path.isabs(filename): for path in [os.curdir] + sys.path: if path is None: @@ -69,9 +70,9 @@ def canonical_filename(filename): except UnicodeError: exists = False if exists: - filename = f + cf = f break - cf = abs_file(filename) + cf = abs_file(cf) CANONICAL_FILENAME_CACHE[filename] = cf return CANONICAL_FILENAME_CACHE[filename] @@ -122,7 +123,9 @@ def actual_path(path): else: try: files = os.listdir(head) - except OSError: + except Exception: + # This will raise OSError, or this bizarre TypeError: + # https://bugs.python.org/issue1776160 files = [] _ACTUAL_PATH_LIST_CACHE[head] = files normtail = os.path.normcase(tail) @@ -156,9 +159,8 @@ def unicode_filename(filename): @contract(returns='unicode') -def abs_file(filename): - """Return the absolute normalized form of `filename`.""" - path = os.path.expandvars(os.path.expanduser(filename)) +def abs_file(path): + """Return the absolute normalized form of `path`.""" try: path = os.path.realpath(path) except UnicodeError: @@ -169,6 +171,13 @@ def abs_file(filename): return path +def python_reported_file(filename): + """Return the string as Python would describe this file name.""" + if env.PYBEHAVIOR.report_absolute_files: + filename = os.path.abspath(filename) + return filename + + RELATIVE_DIR = None CANONICAL_FILENAME_CACHE = None set_relative_directory() @@ -260,19 +269,8 @@ def match(self, module_name): class FnmatchMatcher(object): """A matcher for files by file name pattern.""" def __init__(self, pats): - self.pats = pats[:] - # fnmatch is platform-specific. On Windows, it does the Windows thing - # of treating / and \ as equivalent. But on other platforms, we need to - # take care of that ourselves. - fnpats = (fnmatch.translate(p) for p in pats) - # Python3.7 fnmatch translates "/" as "/", before that, it translates as "\/", - # so we have to deal with maybe a backslash. - fnpats = (re.sub(r"\\?/", r"[\\\\/]", p) for p in fnpats) - flags = 0 - if env.WINDOWS: - # Windows is also case-insensitive, so make the regex case-insensitive. - flags |= re.IGNORECASE - self.re = re.compile(join_regex(fnpats), flags=flags) + self.pats = list(pats) + self.re = fnmatches_to_regex(self.pats, case_insensitive=env.WINDOWS) def __repr__(self): return "" % self.pats @@ -296,6 +294,39 @@ def sep(s): return the_sep +def fnmatches_to_regex(patterns, case_insensitive=False, partial=False): + """Convert fnmatch patterns to a compiled regex that matches any of them. + + Slashes are always converted to match either slash or backslash, for + Windows support, even when running elsewhere. + + If `partial` is true, then the pattern will match if the target string + starts with the pattern. Otherwise, it must match the entire string. + + Returns: a compiled regex object. Use the .match method to compare target + strings. + + """ + regexes = (fnmatch.translate(pattern) for pattern in patterns) + # Python3.7 fnmatch translates "/" as "/". Before that, it translates as "\/", + # so we have to deal with maybe a backslash. + regexes = (re.sub(r"\\?/", r"[\\\\/]", regex) for regex in regexes) + + if partial: + # fnmatch always adds a \Z to match the whole string, which we don't + # want, so we remove the \Z. While removing it, we only replace \Z if + # followed by paren (introducing flags), or at end, to keep from + # destroying a literal \Z in the pattern. + regexes = (re.sub(r'\\Z(\(\?|$)', r'\1', regex) for regex in regexes) + + flags = 0 + if case_insensitive: + flags |= re.IGNORECASE + compiled = re.compile(join_regex(regexes), flags=flags) + + return compiled + + class PathAliases(object): """A collection of aliases for paths. @@ -313,7 +344,7 @@ def __init__(self): def pprint(self): # pragma: debugging """Dump the important parts of the PathAliases, for debugging.""" for regex, result in self.aliases: - print("{0!r} --> {1!r}".format(regex.pattern, result)) + print("{!r} --> {!r}".format(regex.pattern, result)) def add(self, pattern, result): """Add the `pattern`/`result` pair to the list of aliases. @@ -343,18 +374,8 @@ def add(self, pattern, result): if not pattern.endswith(pattern_sep): pattern += pattern_sep - # Make a regex from the pattern. fnmatch always adds a \Z to - # match the whole string, which we don't want, so we remove the \Z. - # While removing it, we only replace \Z if followed by paren, or at - # end, to keep from destroying a literal \Z in the pattern. - regex_pat = fnmatch.translate(pattern) - regex_pat = re.sub(r'\\Z(\(|$)', r'\1', regex_pat) - - # We want */a/b.py to match on Windows too, so change slash to match - # either separator. - regex_pat = regex_pat.replace(r"\/", r"[\\/]") - # We want case-insensitive matching, so add that flag. - regex = re.compile(r"(?i)" + regex_pat) + # Make a regex from the pattern. + regex = fnmatches_to_regex([pattern], case_insensitive=True, partial=True) # Normalize the result: it must end with a path separator. result_sep = sep(result) diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py index 699f38634..aeb416e40 100644 --- a/coverage/fullcoverage/encodings.py +++ b/coverage/fullcoverage/encodings.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Imposter encodings module that installs a coverage-style tracer. diff --git a/coverage/html.py b/coverage/html.py index b0c616499..3596bbe1d 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -1,19 +1,21 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """HTML reporting for coverage.py.""" import datetime import json import os +import re import shutil import coverage from coverage import env -from coverage.backward import iitems +from coverage.backward import iitems, SimpleNamespace, format_local_datetime +from coverage.data import add_data_to_hash from coverage.files import flat_rootname -from coverage.misc import CoverageException, file_be_gone, Hasher, isolate_module -from coverage.report import Reporter +from coverage.misc import CoverageException, ensure_dir, file_be_gone, Hasher, isolate_module +from coverage.report import get_analysis_to_report from coverage.results import Numbers from coverage.templite import Templite @@ -66,11 +68,90 @@ def read_data(fname): def write_html(fname, html): """Write `html` to `fname`, properly encoded.""" + html = re.sub(r"(\A\s+)|(\s+$)", "", html, flags=re.MULTILINE) + "\n" with open(fname, "wb") as fout: fout.write(html.encode('ascii', 'xmlcharrefreplace')) -class HtmlReporter(Reporter): +class HtmlDataGeneration(object): + """Generate structured data to be turned into HTML reports.""" + + EMPTY = "(empty)" + + def __init__(self, cov): + self.coverage = cov + self.config = self.coverage.config + data = self.coverage.get_data() + self.has_arcs = data.has_arcs() + if self.config.show_contexts: + if data.measured_contexts() == set([""]): + self.coverage._warn("No contexts were measured") + data.set_query_contexts(self.config.report_contexts) + + def data_for_file(self, fr, analysis): + """Produce the data needed for one file's report.""" + if self.has_arcs: + missing_branch_arcs = analysis.missing_branch_arcs() + arcs_executed = analysis.arcs_executed() + + if self.config.show_contexts: + contexts_by_lineno = analysis.data.contexts_by_lineno(analysis.filename) + + lines = [] + + for lineno, tokens in enumerate(fr.source_token_lines(), start=1): + # Figure out how to mark this line. + category = None + short_annotations = [] + long_annotations = [] + + if lineno in analysis.excluded: + category = 'exc' + elif lineno in analysis.missing: + category = 'mis' + elif self.has_arcs and lineno in missing_branch_arcs: + category = 'par' + for b in missing_branch_arcs[lineno]: + if b < 0: + short_annotations.append("exit") + else: + short_annotations.append(b) + long_annotations.append(fr.missing_arc_description(lineno, b, arcs_executed)) + elif lineno in analysis.statements: + category = 'run' + + contexts = contexts_label = None + context_list = None + if category and self.config.show_contexts: + contexts = sorted(c or self.EMPTY for c in contexts_by_lineno[lineno]) + if contexts == [self.EMPTY]: + contexts_label = self.EMPTY + else: + contexts_label = "{} ctx".format(len(contexts)) + context_list = contexts + + lines.append(SimpleNamespace( + tokens=tokens, + number=lineno, + category=category, + statement=(lineno in analysis.statements), + contexts=contexts, + contexts_label=contexts_label, + context_list=context_list, + short_annotations=short_annotations, + long_annotations=long_annotations, + )) + + file_data = SimpleNamespace( + relative_filename=fr.relative_filename(), + nums=analysis.numbers, + lines=lines, + ) + + return file_data + + +class HtmlReporter(object): """HTML reporting.""" # These files will be copied from the htmlfiles directory to the output @@ -87,30 +168,54 @@ class HtmlReporter(Reporter): ("keybd_open.png", ""), ] - def __init__(self, cov, config): - super(HtmlReporter, self).__init__(cov, config) - self.directory = None + def __init__(self, cov): + self.coverage = cov + self.config = self.coverage.config + self.directory = self.config.html_dir title = self.config.html_title if env.PY2: title = title.decode("utf8") + + if self.config.extra_css: + self.extra_css = os.path.basename(self.config.extra_css) + else: + self.extra_css = None + + self.data = self.coverage.get_data() + self.has_arcs = self.data.has_arcs() + + self.file_summaries = [] + self.all_files_nums = [] + self.incr = IncrementalChecker(self.directory) + self.datagen = HtmlDataGeneration(self.coverage) + self.totals = Numbers() + self.template_globals = { + # Functions available in the templates. 'escape': escape, 'pair': pair, - 'title': title, + 'len': len, + + # Constants for this report. '__url__': coverage.__url__, '__version__': coverage.__version__, + 'title': title, + 'time_stamp': format_local_datetime(datetime.datetime.now()), + 'extra_css': self.extra_css, + 'has_arcs': self.has_arcs, + 'show_contexts': self.config.show_contexts, + + # Constants for all reports. + # These css classes determine which lines are highlighted by default. + 'category': { + 'exc': 'exc show_exc', + 'mis': 'mis show_mis', + 'par': 'par run show_par', + 'run': 'run', + } } - self.source_tmpl = Templite(read_data("pyfile.html"), self.template_globals) - - self.coverage = cov - - self.files = [] - self.all_files_nums = [] - self.has_arcs = self.coverage.data.has_arcs() - self.status = HtmlStatus() - self.extra_css = None - self.totals = Numbers() - self.time_stamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M') + self.pyfile_html_source = read_data("pyfile.html") + self.source_tmpl = Templite(self.pyfile_html_source, self.template_globals) def report(self, morfs): """Generate an HTML report for `morfs`. @@ -118,29 +223,20 @@ def report(self, morfs): `morfs` is a list of modules or file names. """ - assert self.config.html_dir, "must give a directory for html reporting" - - # Read the status data. - self.status.read(self.config.html_dir) - - # Check that this run used the same settings as the last run. - m = Hasher() - m.update(self.config) - these_settings = m.hexdigest() - if self.status.settings_hash() != these_settings: - self.status.reset() - self.status.set_settings_hash(these_settings) - - # The user may have extra CSS they want copied. - if self.config.extra_css: - self.extra_css = os.path.basename(self.config.extra_css) + # Read the status data and check that this run used the same + # global data as the last run. + self.incr.read() + self.incr.check_global_data(self.config, self.pyfile_html_source) # Process all the files. - self.report_files(self.html_file, morfs, self.config.html_dir) + for fr, analysis in get_analysis_to_report(self.coverage, morfs): + self.html_file(fr, analysis) if not self.all_files_nums: raise CoverageException("No data to report.") + self.totals = sum(self.all_files_nums) + # Write the index file. self.index_file() @@ -163,17 +259,11 @@ def make_local_static_report_files(self): os.path.join(self.directory, self.extra_css) ) - def file_hash(self, source, fr): - """Compute a hash that changes if the file needs to be re-reported.""" - m = Hasher() - m.update(source) - self.coverage.data.add_to_hash(fr.filename, m) - return m.hexdigest() - def html_file(self, fr, analysis): """Generate an HTML file for one source file.""" rootname = flat_rootname(fr.relative_filename()) html_filename = rootname + ".html" + ensure_dir(self.directory) html_path = os.path.join(self.directory, html_filename) # Get the numbers for this file. @@ -189,100 +279,63 @@ def html_file(self, fr, analysis): file_be_gone(html_path) return - source = fr.source() + if self.config.skip_empty: + # Don't report on empty files. + if nums.n_statements == 0: + file_be_gone(html_path) + return # Find out if the file on disk is already correct. - this_hash = self.file_hash(source.encode('utf-8'), fr) - that_hash = self.status.file_hash(rootname) - if this_hash == that_hash: - # Nothing has changed to require the file to be reported again. - self.files.append(self.status.index_info(rootname)) + if self.incr.can_skip_file(self.data, fr, rootname): + self.file_summaries.append(self.incr.index_info(rootname)) return - self.status.set_file_hash(rootname, this_hash) - - if self.has_arcs: - missing_branch_arcs = analysis.missing_branch_arcs() - arcs_executed = analysis.arcs_executed() - - # These classes determine which lines are highlighted by default. - c_run = "run hide_run" - c_exc = "exc" - c_mis = "mis" - c_par = "par " + c_run - - lines = [] - - for lineno, line in enumerate(fr.source_token_lines(), start=1): - # Figure out how to mark this line. - line_class = [] - annotate_html = "" - annotate_long = "" - if lineno in analysis.statements: - line_class.append("stm") - if lineno in analysis.excluded: - line_class.append(c_exc) - elif lineno in analysis.missing: - line_class.append(c_mis) - elif self.has_arcs and lineno in missing_branch_arcs: - line_class.append(c_par) - shorts = [] - longs = [] - for b in missing_branch_arcs[lineno]: - if b < 0: - shorts.append("exit") - else: - shorts.append(b) - longs.append(fr.missing_arc_description(lineno, b, arcs_executed)) - # 202F is NARROW NO-BREAK SPACE. - # 219B is RIGHTWARDS ARROW WITH STROKE. - short_fmt = "%s ↛ %s" - annotate_html = ",   ".join(short_fmt % (lineno, d) for d in shorts) - - if len(longs) == 1: - annotate_long = longs[0] - else: - annotate_long = "%d missed branches: %s" % ( - len(longs), - ", ".join("%d) %s" % (num, ann_long) - for num, ann_long in enumerate(longs, start=1)), - ) - elif lineno in analysis.statements: - line_class.append(c_run) - + # Write the HTML page for this file. + file_data = self.datagen.data_for_file(fr, analysis) + for ldata in file_data.lines: # Build the HTML for the line. html = [] - for tok_type, tok_text in line: + for tok_type, tok_text in ldata.tokens: if tok_type == "ws": html.append(escape(tok_text)) else: tok_html = escape(tok_text) or ' ' html.append( - '%s' % (tok_type, tok_html) + u'{}'.format(tok_type, tok_html) + ) + ldata.html = ''.join(html) + + if ldata.short_annotations: + # 202F is NARROW NO-BREAK SPACE. + # 219B is RIGHTWARDS ARROW WITH STROKE. + ldata.annotate = u",   ".join( + u"{} ↛ {}".format(ldata.number, d) + for d in ldata.short_annotations ) + else: + ldata.annotate = None - lines.append({ - 'html': ''.join(html), - 'number': lineno, - 'class': ' '.join(line_class) or "pln", - 'annotate': annotate_html, - 'annotate_long': annotate_long, - }) + if ldata.long_annotations: + longs = ldata.long_annotations + if len(longs) == 1: + ldata.annotate_long = longs[0] + else: + ldata.annotate_long = u"{:d} missed branches: {}".format( + len(longs), + u", ".join( + u"{:d}) {}".format(num, ann_long) + for num, ann_long in enumerate(longs, start=1) + ), + ) + else: + ldata.annotate_long = None - # Write the HTML page for this file. - html = self.source_tmpl.render({ - 'c_exc': c_exc, - 'c_mis': c_mis, - 'c_par': c_par, - 'c_run': c_run, - 'has_arcs': self.has_arcs, - 'extra_css': self.extra_css, - 'fr': fr, - 'nums': nums, - 'lines': lines, - 'time_stamp': self.time_stamp, - }) + css_classes = [] + if ldata.category: + css_classes.append(self.template_globals['category'][ldata.category]) + ldata.css_class = ' '.join(css_classes) or "pln" + html = self.source_tmpl.render(file_data.__dict__) write_html(html_path, html) # Save this file's information for the index file. @@ -291,77 +344,73 @@ def html_file(self, fr, analysis): 'html_filename': html_filename, 'relative_filename': fr.relative_filename(), } - self.files.append(index_info) - self.status.set_index_info(rootname, index_info) + self.file_summaries.append(index_info) + self.incr.set_index_info(rootname, index_info) def index_file(self): """Write the index.html file for this report.""" index_tmpl = Templite(read_data("index.html"), self.template_globals) - self.totals = sum(self.all_files_nums) - html = index_tmpl.render({ - 'has_arcs': self.has_arcs, - 'extra_css': self.extra_css, - 'files': self.files, + 'files': self.file_summaries, 'totals': self.totals, - 'time_stamp': self.time_stamp, }) write_html(os.path.join(self.directory, "index.html"), html) # Write the latest hashes for next time. - self.status.write(self.directory) + self.incr.write() -class HtmlStatus(object): - """The status information we keep to support incremental reporting.""" +class IncrementalChecker(object): + """Logic and data to support incremental reporting.""" STATUS_FILE = "status.json" - STATUS_FORMAT = 1 + STATUS_FORMAT = 2 # pylint: disable=wrong-spelling-in-comment,useless-suppression # The data looks like: # # { - # 'format': 1, - # 'settings': '540ee119c15d52a68a53fe6f0897346d', - # 'version': '4.0a1', - # 'files': { - # 'cogapp___init__': { - # 'hash': 'e45581a5b48f879f301c0f30bf77a50c', - # 'index': { - # 'html_filename': 'cogapp___init__.html', - # 'name': 'cogapp/__init__', - # 'nums': , + # "format": 2, + # "globals": "540ee119c15d52a68a53fe6f0897346d", + # "version": "4.0a1", + # "files": { + # "cogapp___init__": { + # "hash": "e45581a5b48f879f301c0f30bf77a50c", + # "index": { + # "html_filename": "cogapp___init__.html", + # "relative_filename": "cogapp/__init__", + # "nums": [ 1, 14, 0, 0, 0, 0, 0 ] # } # }, # ... - # 'cogapp_whiteutils': { - # 'hash': '8504bb427fc488c4176809ded0277d51', - # 'index': { - # 'html_filename': 'cogapp_whiteutils.html', - # 'name': 'cogapp/whiteutils', - # 'nums': , + # "cogapp_whiteutils": { + # "hash": "8504bb427fc488c4176809ded0277d51", + # "index": { + # "html_filename": "cogapp_whiteutils.html", + # "relative_filename": "cogapp/whiteutils", + # "nums": [ 1, 59, 0, 1, 28, 2, 2 ] # } - # }, - # }, + # } + # } # } - def __init__(self): + def __init__(self, directory): + self.directory = directory self.reset() def reset(self): - """Initialize to empty.""" - self.settings = '' + """Initialize to empty. Causes all files to be reported.""" + self.globals = '' self.files = {} - def read(self, directory): - """Read the last status in `directory`.""" + def read(self): + """Read the information we stored last time.""" usable = False try: - status_file = os.path.join(directory, self.STATUS_FILE) - with open(status_file, "r") as fstatus: + status_file = os.path.join(self.directory, self.STATUS_FILE) + with open(status_file) as fstatus: status = json.load(fstatus) except (IOError, ValueError): usable = False @@ -377,13 +426,13 @@ def read(self, directory): for filename, fileinfo in iitems(status['files']): fileinfo['index']['nums'] = Numbers(*fileinfo['index']['nums']) self.files[filename] = fileinfo - self.settings = status['settings'] + self.globals = status['globals'] else: self.reset() - def write(self, directory): - """Write the current status to `directory`.""" - status_file = os.path.join(directory, self.STATUS_FILE) + def write(self): + """Write the current status.""" + status_file = os.path.join(self.directory, self.STATUS_FILE) files = {} for filename, fileinfo in iitems(self.files): fileinfo['index']['nums'] = fileinfo['index']['nums'].init_args() @@ -392,26 +441,41 @@ def write(self, directory): status = { 'format': self.STATUS_FORMAT, 'version': coverage.__version__, - 'settings': self.settings, + 'globals': self.globals, 'files': files, } with open(status_file, "w") as 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. - # https://issues.jenkins-ci.org/browse/JENKINS-28428 - if "JENKINS_URL" in os.environ: - with open(os.path.join(directory, "status.dat"), "w") as dat: - dat.write("https://issues.jenkins-ci.org/browse/JENKINS-28428\n") + def check_global_data(self, *data): + """Check the global data that can affect incremental reporting.""" + m = Hasher() + for d in data: + m.update(d) + these_globals = m.hexdigest() + if self.globals != these_globals: + self.reset() + self.globals = these_globals - def settings_hash(self): - """Get the hash of the coverage.py settings.""" - return self.settings + def can_skip_file(self, data, fr, rootname): + """Can we skip reporting this file? - def set_settings_hash(self, settings): - """Set the hash of the coverage.py settings.""" - self.settings = settings + `data` is a CoverageData object, `fr` is a `FileReporter`, and + `rootname` is the name being used for the file. + """ + m = Hasher() + m.update(fr.source().encode('utf-8')) + add_data_to_hash(data, fr.filename, m) + this_hash = m.hexdigest() + + that_hash = self.file_hash(rootname) + + if this_hash == that_hash: + # Nothing has changed to require the file to be reported again. + return True + else: + self.set_file_hash(rootname, this_hash) + return False def file_hash(self, fname): """Get the hash of `fname`'s contents.""" diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index f6f5de207..3bf04bf92 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -1,5 +1,5 @@ // Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt // Coverage.py HTML report browser code. /*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ @@ -169,22 +169,16 @@ coverage.wire_up_filter = function () { // Loaded on index.html coverage.index_ready = function ($) { - // Look for a cookie containing previous sort settings: + // Look for a localStorage item containing previous sort settings: var sort_list = []; - var cookie_name = "COVERAGE_INDEX_SORT"; - var i; - - // This almost makes it worth installing the jQuery cookie plugin: - if (document.cookie.indexOf(cookie_name) > -1) { - var cookies = document.cookie.split(";"); - for (i = 0; i < cookies.length; i++) { - var parts = cookies[i].split("="); - - if ($.trim(parts[0]) === cookie_name && parts[1]) { - sort_list = eval("[[" + parts[1] + "]]"); - break; - } - } + var storage_name = "COVERAGE_INDEX_SORT"; + var stored_list = undefined; + try { + stored_list = localStorage.getItem(storage_name); + } catch(err) {} + + if (stored_list) { + sort_list = JSON.parse('[[' + stored_list + ']]'); } // Create a new widget which exists only to save and restore @@ -231,7 +225,9 @@ coverage.index_ready = function ($) { // Watch for page unload events so we can save the final sort settings: $(window).unload(function () { - document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + try { + localStorage.setItem(storage_name, sort_list.toString()) + } catch(err) {} }); }; @@ -240,7 +236,7 @@ coverage.index_ready = function ($) { coverage.pyfile_ready = function ($) { // If we're directed to a particular line number, highlight the line. var frag = location.hash; - if (frag.length > 2 && frag[1] === 'n') { + if (frag.length > 2 && frag[1] === 't') { $(frag).addClass('highlight'); coverage.set_sel(parseInt(frag.substr(2), 10)); } @@ -265,21 +261,22 @@ coverage.pyfile_ready = function ($) { coverage.init_scroll_markers(); - // Rebuild scroll markers after window high changing - $(window).resize(coverage.resize_scroll_markers); + // Rebuild scroll markers when the window height changes. + $(window).resize(coverage.build_scroll_markers); }; coverage.toggle_lines = function (btn, cls) { btn = $(btn); - var hide = "hide_"+cls; - if (btn.hasClass(hide)) { - $("#source ."+cls).removeClass(hide); - btn.removeClass(hide); + var show = "show_"+cls; + if (btn.hasClass(show)) { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); } else { - $("#source ."+cls).addClass(hide); - btn.addClass(hide); + $("#source ." + cls).addClass(show); + btn.addClass(show); } + coverage.build_scroll_markers(); }; // Return the nth line div. @@ -292,11 +289,6 @@ coverage.num_elt = function (n) { return $("#n" + n); }; -// Return the container of all the code. -coverage.code_container = function () { - return $(".linenos"); -}; - // Set the selection. b and e are line numbers. coverage.set_sel = function (b, e) { // The first line selected. @@ -315,9 +307,17 @@ coverage.to_first_chunk = function () { coverage.to_next_chunk(); }; -coverage.is_transparent = function (color) { - // Different browsers return different colors for "none". - return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + var klass = line_elt.attr('class'); + if (klass) { + var m = klass.match(/\bshow_\w+\b/); + if (m) { + return m[0]; + } + } + return null; }; coverage.to_next_chunk = function () { @@ -325,14 +325,14 @@ coverage.to_next_chunk = function () { // Find the start of the next colored chunk. var probe = c.sel_end; - var color, probe_line; + var chunk_indicator, probe_line; while (true) { probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } - color = probe_line.css("background-color"); - if (!c.is_transparent(color)) { + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { break; } probe++; @@ -342,11 +342,11 @@ coverage.to_next_chunk = function () { var begin = probe; // Find the end of this chunk. - var next_color = color; - while (next_color === color) { + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { probe++; probe_line = c.line_elt(probe); - next_color = probe_line.css("background-color"); + next_indicator = c.chunk_indicator(probe_line); } c.set_sel(begin, probe); c.show_selection(); @@ -361,25 +361,25 @@ coverage.to_prev_chunk = function () { if (probe_line.length === 0) { return; } - var color = probe_line.css("background-color"); - while (probe > 0 && c.is_transparent(color)) { + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 0 && !chunk_indicator) { probe--; probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } - color = probe_line.css("background-color"); + chunk_indicator = c.chunk_indicator(probe_line); } // There's a prev chunk, `probe` points to its last line. var end = probe+1; // Find the beginning of this chunk. - var prev_color = color; - while (prev_color === color) { + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { probe--; probe_line = c.line_elt(probe); - prev_color = probe_line.css("background-color"); + prev_indicator = c.chunk_indicator(probe_line); } c.set_sel(probe+1, end); c.show_selection(); @@ -451,29 +451,29 @@ coverage.select_line_or_chunk = function (lineno) { if (probe_line.length === 0) { return; } - var the_color = probe_line.css("background-color"); - if (!c.is_transparent(the_color)) { + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { // The line is in a highlighted chunk. // Search backward for the first line. var probe = lineno; - var color = the_color; - while (probe > 0 && color === the_color) { + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { probe--; probe_line = c.line_elt(probe); if (probe_line.length === 0) { break; } - color = probe_line.css("background-color"); + indicator = c.chunk_indicator(probe_line); } var begin = probe + 1; // Search forward for the last line. probe = lineno; - color = the_color; - while (color === the_color) { + indicator = the_indicator; + while (indicator === the_indicator) { probe++; probe_line = c.line_elt(probe); - color = probe_line.css("background-color"); + indicator = c.chunk_indicator(probe_line); } coverage.set_sel(begin, probe); @@ -487,7 +487,7 @@ coverage.show_selection = function () { var c = coverage; // Highlight the lines in the chunk - c.code_container().find(".highlight").removeClass("highlight"); + $(".linenos .highlight").removeClass("highlight"); for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { c.num_elt(probe).addClass("highlight"); } @@ -517,21 +517,21 @@ coverage.finish_scrolling = function () { coverage.init_scroll_markers = function () { var c = coverage; // Init some variables - c.lines_len = $('td.text p').length; + c.lines_len = $('#source p').length; c.body_h = $('body').height(); c.header_h = $('div#header').height(); - c.missed_lines = $('td.text p.mis, td.text p.par'); // Build html - c.resize_scroll_markers(); + c.build_scroll_markers(); }; -coverage.resize_scroll_markers = function () { +coverage.build_scroll_markers = function () { var c = coverage, min_line_height = 3, max_line_height = 10, visible_window_h = $(window).height(); + c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'); $('#scroll_marker').remove(); // Don't build markers if the window has no scroll bar. if (c.body_h <= visible_window_h) { @@ -555,11 +555,16 @@ coverage.resize_scroll_markers = function () { var previous_line = -99, last_mark, - last_top; + last_top, + offsets = {}; - c.missed_lines.each(function () { - var line_top = Math.round($(this).offset().top * marker_scale), - id_name = $(this).attr('id'), + // Calculate line offsets outside loop to prevent relayouts + c.lines_to_mark.each(function() { + offsets[this.id] = $(this).offset().top; + }); + c.lines_to_mark.each(function () { + var id_name = $(this).attr('id'), + line_top = Math.round(offsets[id_name] * marker_scale), line_number = parseInt(id_name.substring(1, id_name.length)); if (line_number === previous_line + 1) { diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html index 1e3999f9e..4129bc31b 100644 --- a/coverage/htmlfiles/index.html +++ b/coverage/htmlfiles/index.html @@ -1,5 +1,5 @@ {# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 #} -{# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt #} +{# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt #} diff --git a/coverage/htmlfiles/keybd_closed.png b/coverage/htmlfiles/keybd_closed.png old mode 100755 new mode 100644 diff --git a/coverage/htmlfiles/keybd_open.png b/coverage/htmlfiles/keybd_open.png old mode 100755 new mode 100644 diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index 8542a4678..ec0f416ff 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -1,5 +1,5 @@ {# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 #} -{# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt #} +{# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt #} @@ -8,7 +8,7 @@ {# IE8 rounds line-height incorrectly, and adding this emulateIE7 line makes it right! #} {# http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/7684445e-f080-4d8f-8529-132763348e21 #} - Coverage for {{fr.relative_filename|escape}}: {{nums.pc_covered_str}}% + Coverage for {{relative_filename|escape}}: {{nums.pc_covered_str}}% {% if extra_css %} @@ -25,7 +25,7 @@
- - - - - -
-{% for line in lines -%} -

{{line.number}}

-{% endfor %} -
-{# These are the source lines, which are very sensitive to whitespace. -#} -{# The `{ # - # }` below are comments which slurp up the following space. -#} -{% for line in lines -%} -

{#-#} - {% if line.annotate -%} - {{line.annotate}}{#-#} - {{line.annotate_long}}{#-#} - {% endif -%} - {{line.html}} {#-#} -

-{% endfor %} -
+ {% for line in lines -%} + {% joined %} +

+ {{line.number}} + {{line.html}}  + {% if line.context_list %} + + {% endif %} + {# Things that should float right in the line. #} + + {% if line.annotate %} + {{line.annotate}} + {{line.annotate_long}} + {% endif %} + {% if line.contexts %} + + {% endif %} + + {# Things that should appear below the line. #} + {% if line.context_list %} + + {% for context in line.context_list %} + {{context}} + {% endfor %} + + {% endif %} +

+ {% endjoined %} + {% endfor %}
-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

- -
-

""" Cog code generation tool. 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2015, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import 

-

 

-

from .cogapp import * 

- -
+

1""" Cog content generation tool. 

+

2 http://nedbatchelder.com/code/cog 

+

3 

+

4 Copyright 2004-2015, Ned Batchelder. 

+

5""" 

+

6 

+

7from __future__ import absolute_import 

+

8 

+

9from .cogapp import * 

- - diff --git a/doc/sample_html/cogapp___main___py.html b/doc/sample_html/cogapp___main___py.html index 0306c9d67..6110e8887 100644 --- a/doc/sample_html/cogapp___main___py.html +++ b/doc/sample_html/cogapp___main___py.html @@ -1,16 +1,10 @@ - - - - - Coverage for cogapp/__main__.py: 0.00% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,38 +51,20 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

"""Make Cog runnable directly from the module.""" 

-

import sys 

-

from cogapp import Cog 

-

 

-

sys.exit(Cog().main(sys.argv)) 

- -
+

1"""Make Cog runnable directly from the module.""" 

+

2import sys 

+

3from cogapp import Cog 

+

4 

+

5sys.exit(Cog().main(sys.argv)) 

- - diff --git a/doc/sample_html/cogapp_backward_py.html b/doc/sample_html/cogapp_backward_py.html index 0b672d551..3cc5ceb52 100644 --- a/doc/sample_html/cogapp_backward_py.html +++ b/doc/sample_html/cogapp_backward_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/backward.py: 57.14% + Coverage for cogapp/backward.py: 69.23% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,80 +51,57 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

- -
-

"""Compatibility between Py2 and Py3.""" 

-

 

-

import sys 

-

 

-

PY3 = sys.version_info[0] == 3 

-

 

-

7 ↛ 8line 7 didn't jump to line 8, because the condition on line 7 was never trueif PY3: 

-

string_types = (str,bytes) 

-

bytes_types = (bytes,) 

-

def b(s): 

-

return s.encode("latin-1") 

-

def to_bytes(s): 

-

return s.encode('utf8') 

-

else: 

-

string_types = (basestring,) 

-

bytes_types = (str,) 

-

def b(s): 

-

return s 

-

def to_bytes(s): 

-

return s 

-

 

-

# Pythons 2 and 3 differ on where to get StringIO 

-

try: 

-

from cStringIO import StringIO 

-

except ImportError: 

-

from io import StringIO 

- -
+

1"""Compatibility between Py2 and Py3.""" 

+

2 

+

3import sys 

+

4import unittest 

+

5 

+

6PY3 = sys.version_info[0] == 3 

+

7 

+

8if PY3: 8 ↛ 14line 8 didn't jump to line 14, because the condition on line 8 was never false

+

9 string_types = (str,bytes) 

+

10 bytes_types = (bytes,) 

+

11 def to_bytes(s): 

+

12 return s.encode('utf8') 

+

13else: 

+

14 string_types = (basestring,) 

+

15 bytes_types = (str,) 

+

16 def to_bytes(s): 

+

17 return s 

+

18 

+

19# Pythons 2 and 3 differ on where to get StringIO 

+

20try: 

+

21 from cStringIO import StringIO 

+

22except ImportError: 

+

23 from io import StringIO 

+

24 

+

25 

+

26def unittest_has(method): 

+

27 """Does `unittest.TestCase` have `method` defined?""" 

+

28 return hasattr(unittest.TestCase, method) 

+

29 

+

30 

+

31class TestCase(unittest.TestCase): 

+

32 """Just like unittest.TestCase, but with assert methods added. 

+

33 

+

34 Designed to be compatible with 3.1 unittest. Methods are only defined if 

+

35 `unittest` doesn't have them. 

+

36 

+

37 """ 

+

38 # pylint: disable=missing-docstring 

+

39 

+

40 if not unittest_has('assertRaisesRegex'): 40 ↛ 41line 40 didn't jump to line 41, because the condition on line 40 was never true

+

41 def assertRaisesRegex(self, *args, **kwargs): 

+

42 return self.assertRaisesRegexp(*args, **kwargs) 

- - diff --git a/doc/sample_html/cogapp_cogapp_py.html b/doc/sample_html/cogapp_cogapp_py.html index 75f1d0860..a9c893070 100644 --- a/doc/sample_html/cogapp_cogapp_py.html +++ b/doc/sample_html/cogapp_cogapp_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/cogapp.py: 98.84% + Coverage for cogapp/cogapp.py: 49.34% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,1590 +51,823 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

-

500

-

501

-

502

-

503

-

504

-

505

-

506

-

507

-

508

-

509

-

510

-

511

-

512

-

513

-

514

-

515

-

516

-

517

-

518

-

519

-

520

-

521

-

522

-

523

-

524

-

525

-

526

-

527

-

528

-

529

-

530

-

531

-

532

-

533

-

534

-

535

-

536

-

537

-

538

-

539

-

540

-

541

-

542

-

543

-

544

-

545

-

546

-

547

-

548

-

549

-

550

-

551

-

552

-

553

-

554

-

555

-

556

-

557

-

558

-

559

-

560

-

561

-

562

-

563

-

564

-

565

-

566

-

567

-

568

-

569

-

570

-

571

-

572

-

573

-

574

-

575

-

576

-

577

-

578

-

579

-

580

-

581

-

582

-

583

-

584

-

585

-

586

-

587

-

588

-

589

-

590

-

591

-

592

-

593

-

594

-

595

-

596

-

597

-

598

-

599

-

600

-

601

-

602

-

603

-

604

-

605

-

606

-

607

-

608

-

609

-

610

-

611

-

612

-

613

-

614

-

615

-

616

-

617

-

618

-

619

-

620

-

621

-

622

-

623

-

624

-

625

-

626

-

627

-

628

-

629

-

630

-

631

-

632

-

633

-

634

-

635

-

636

-

637

-

638

-

639

-

640

-

641

-

642

-

643

-

644

-

645

-

646

-

647

-

648

-

649

-

650

-

651

-

652

-

653

-

654

-

655

-

656

-

657

-

658

-

659

-

660

-

661

-

662

-

663

-

664

-

665

-

666

-

667

-

668

-

669

-

670

-

671

-

672

-

673

-

674

-

675

-

676

-

677

-

678

-

679

-

680

-

681

-

682

-

683

-

684

-

685

-

686

-

687

-

688

-

689

-

690

-

691

-

692

-

693

-

694

-

695

-

696

-

697

-

698

-

699

-

700

-

701

-

702

-

703

-

704

-

705

-

706

-

707

-

708

-

709

-

710

-

711

-

712

-

713

-

714

-

715

-

716

-

717

-

718

-

719

-

720

-

721

-

722

-

723

-

724

-

725

-

726

-

727

-

728

-

729

-

730

-

731

-

732

-

733

-

734

-

735

-

736

-

737

-

738

-

739

-

740

-

741

-

742

-

743

-

744

-

745

-

746

-

747

-

748

-

749

-

750

-

751

-

752

-

753

-

754

-

755

-

756

-

757

-

758

-

759

-

760

-

761

-

762

-

763

-

764

-

765

-

766

-

767

-

768

-

769

-

770

-

771

-

772

-

773

-

774

-

775

-

776

-

777

-

778

-

779

-

780

-

781

- -
-

# coding: utf8 

-

""" Cog code generation tool. 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2016, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import, print_function 

-

 

-

import copy, getopt, hashlib, imp, os, re, shlex, sys, traceback 

-

from .backward import PY3, StringIO, string_types, to_bytes 

-

 

-

__all__ = ['Cog', 'CogUsageError'] 

-

 

-

__version__ = '2.5.1' # History at the end of the file. 

-

 

-

usage = """\ 

-

cog - generate code with inlined Python code. 

-

 

-

cog [OPTIONS] [INFILE | @FILELIST] ... 

-

 

-

INFILE is the name of an input file, '-' will read from stdin. 

-

FILELIST is the name of a text file containing file names or 

-

other @FILELISTs. 

-

 

-

OPTIONS: 

-

-c Checksum the output to protect it against accidental change. 

-

-d Delete the generator code from the output file. 

-

-D name=val Define a global string available to your generator code. 

-

-e Warn if a file has no cog code in it. 

-

-I PATH Add PATH to the list of directories for data files and modules. 

-

-n ENCODING Use ENCODING when reading and writing files. 

-

-o OUTNAME Write the output to OUTNAME. 

-

-r Replace the input file with the output. 

-

-s STRING Suffix all generated output lines with STRING. 

-

-U Write the output with Unix newlines (only LF line-endings). 

-

-w CMD Use CMD if the output file needs to be made writable. 

-

A %s in the CMD will be filled with the filename. 

-

-x Excise all the generated output without running the generators. 

-

-z The end-output marker can be omitted, and is assumed at eof. 

-

-v Print the version of cog and exit. 

-

--verbosity=VERBOSITY 

-

Control the amount of output. 2 (the default) lists all files, 

-

1 lists only changed files, 0 lists no files. 

-

--markers='START END END-OUTPUT' 

-

The patterns surrounding cog inline instructions. Should 

-

include three values separated by spaces, the start, end, 

-

and end-output markers. Defaults to '[[[cog ]]] [[[end]]]'. 

-

-h Print this help. 

-

""" 

-

 

-

# Other package modules 

-

from .whiteutils import * 

-

 

-

class CogError(Exception): 

-

""" Any exception raised by Cog. 

-

""" 

-

def __init__(self, msg, file='', line=0): 

-

if file: 

-

Exception.__init__(self, "%s(%d): %s" % (file, line, msg)) 

-

else: 

-

Exception.__init__(self, msg) 

-

 

-

class CogUsageError(CogError): 

-

""" An error in usage of command-line arguments in cog. 

-

""" 

-

pass #pragma: no cover 

-

 

-

class CogInternalError(CogError): 

-

""" An error in the coding of Cog. Should never happen. 

-

""" 

-

pass #pragma: no cover 

-

 

-

class CogGeneratedError(CogError): 

-

""" An error raised by a user's cog generator. 

-

""" 

-

pass #pragma: no cover 

-

 

-

class Redirectable: 

-

""" An object with its own stdout and stderr files. 

-

""" 

-

def __init__(self): 

-

self.stdout = sys.stdout 

-

self.stderr = sys.stderr 

-

 

-

def setOutput(self, stdout=None, stderr=None): 

-

""" Assign new files for standard out and/or standard error. 

-

""" 

-

if stdout: 

-

self.stdout = stdout 

-

if stderr: 

-

self.stderr = stderr 

-

 

-

def prout(self, s, end="\n"): 

-

print(s, file=self.stdout, end=end) 

-

 

-

def prerr(self, s, end="\n"): 

-

print(s, file=self.stderr, end=end) 

-

 

-

 

-

class CogGenerator(Redirectable): 

-

""" A generator pulled from a source file. 

-

""" 

-

def __init__(self): 

-

Redirectable.__init__(self) 

-

self.markers = [] 

-

self.lines = [] 

-

 

-

def parseMarker(self, l): 

-

self.markers.append(l) 

-

 

-

def parseLine(self, l): 

-

self.lines.append(l.strip('\n')) 

-

 

-

def getCode(self): 

-

""" Extract the executable Python code from the generator. 

-

""" 

-

# If the markers and lines all have the same prefix 

-

# (end-of-line comment chars, for example), 

-

# then remove it from all the lines. 

-

prefIn = commonPrefix(self.markers + self.lines) 

-

if prefIn: 

-

self.markers = [ l.replace(prefIn, '', 1) for l in self.markers ] 

-

self.lines = [ l.replace(prefIn, '', 1) for l in self.lines ] 

-

 

-

return reindentBlock(self.lines, '') 

-

 

-

def evaluate(self, cog, globals, fname='cog generator'): 

-

# figure out the right whitespace prefix for the output 

-

prefOut = whitePrefix(self.markers) 

-

 

-

intext = self.getCode() 

-

if not intext: 

-

return '' 

-

 

-

# In Python 2.2, the last line has to end in a newline. 

-

intext = "import cog\n" + intext + "\n" 

-

code = compile(intext, str(fname), 'exec') 

-

 

-

# Make sure the "cog" module has our state. 

-

cog.cogmodule.msg = self.msg 

-

cog.cogmodule.out = self.out 

-

cog.cogmodule.outl = self.outl 

-

cog.cogmodule.error = self.error 

-

 

-

self.outstring = '' 

-

eval(code, globals) 

-

 

-

# We need to make sure that the last line in the output 

-

# ends with a newline, or it will be joined to the 

-

# end-output line, ruining cog's idempotency. 

-

if self.outstring and self.outstring[-1] != '\n': 

-

self.outstring += '\n' 

-

 

-

return reindentBlock(self.outstring, prefOut) 

-

 

-

def msg(self, s): 

-

self.prout("Message: "+s) 

-

 

-

def out(self, sOut='', dedent=False, trimblanklines=False): 

-

""" The cog.out function. 

-

""" 

-

if trimblanklines and ('\n' in sOut): 

-

lines = sOut.split('\n') 

-

if lines[0].strip() == '': 

-

del lines[0] 

-

167 ↛ 169line 167 didn't jump to line 169, because the condition on line 167 was never false if lines and lines[-1].strip() == '': 

-

del lines[-1] 

-

sOut = '\n'.join(lines)+'\n' 

-

if dedent: 

-

sOut = reindentBlock(sOut) 

-

self.outstring += sOut 

-

 

-

def outl(self, sOut='', **kw): 

-

""" The cog.outl function. 

-

""" 

-

self.out(sOut, **kw) 

-

self.out('\n') 

-

 

-

def error(self, msg='Error raised by cog generator.'): 

-

""" The cog.error function. 

-

Instead of raising standard python errors, cog generators can use 

-

this function. It will display the error without a scary Python 

-

traceback. 

-

""" 

-

raise CogGeneratedError(msg) 

-

 

-

 

-

class NumberedFileReader: 

-

""" A decorator for files that counts the readline()'s called. 

-

""" 

-

def __init__(self, f): 

-

self.f = f 

-

self.n = 0 

-

 

-

def readline(self): 

-

l = self.f.readline() 

-

if l: 

-

self.n += 1 

-

return l 

-

 

-

def linenumber(self): 

-

return self.n 

-

 

-

 

-

class CogOptions: 

-

""" Options for a run of cog. 

-

""" 

-

def __init__(self): 

-

# Defaults for argument values. 

-

self.args = [] 

-

self.includePath = [] 

-

self.defines = {} 

-

self.bShowVersion = False 

-

self.sMakeWritableCmd = None 

-

self.bReplace = False 

-

self.bNoGenerate = False 

-

self.sOutputName = None 

-

self.bWarnEmpty = False 

-

self.bHashOutput = False 

-

self.bDeleteCode = False 

-

self.bEofCanBeEnd = False 

-

self.sSuffix = None 

-

self.bNewlines = False 

-

self.sBeginSpec = '[[[cog' 

-

self.sEndSpec = ']]]' 

-

self.sEndOutput = '[[[end]]]' 

-

self.sEncoding = "utf-8" 

-

self.verbosity = 2 

-

 

-

def __eq__(self, other): 

-

""" Comparison operator for tests to use. 

-

""" 

-

return self.__dict__ == other.__dict__ 

-

 

-

def clone(self): 

-

""" Make a clone of these options, for further refinement. 

-

""" 

-

return copy.deepcopy(self) 

-

 

-

def addToIncludePath(self, dirs): 

-

""" Add directories to the include path. 

-

""" 

-

dirs = dirs.split(os.pathsep) 

-

self.includePath.extend(dirs) 

-

 

-

def parseArgs(self, argv): 

-

# Parse the command line arguments. 

-

try: 

-

opts, self.args = getopt.getopt( 

-

argv, 

-

'cdD:eI:n:o:rs:Uvw:xz', 

-

[ 

-

'markers=', 

-

'verbosity=', 

-

] 

-

) 

-

except getopt.error as msg: 

-

raise CogUsageError(msg) 

-

 

-

# Handle the command line arguments. 

-

for o, a in opts: 

-

if o == '-c': 

-

self.bHashOutput = True 

-

elif o == '-d': 

-

self.bDeleteCode = True 

-

elif o == '-D': 

-

if a.count('=') < 1: 

-

raise CogUsageError("-D takes a name=value argument") 

-

name, value = a.split('=', 1) 

-

self.defines[name] = value 

-

elif o == '-e': 

-

self.bWarnEmpty = True 

-

elif o == '-I': 

-

self.addToIncludePath(a) 

-

elif o == '-n': 

-

self.sEncoding = a 

-

elif o == '-o': 

-

self.sOutputName = a 

-

elif o == '-r': 

-

self.bReplace = True 

-

elif o == '-s': 

-

self.sSuffix = a 

-

elif o == '-U': 

-

self.bNewlines = True 

-

elif o == '-v': 

-

self.bShowVersion = True 

-

elif o == '-w': 

-

self.sMakeWritableCmd = a 

-

elif o == '-x': 

-

self.bNoGenerate = True 

-

elif o == '-z': 

-

self.bEofCanBeEnd = True 

-

elif o == '--markers': 

-

self._parse_markers(a) 

-

elif o == '--verbosity': 

-

self.verbosity = int(a) 

-

else: 

-

# Since getopt.getopt is given a list of possible flags, 

-

# this is an internal error. 

-

raise CogInternalError("Don't understand argument %s" % o) 

-

 

-

def _parse_markers(self, val): 

-

try: 

-

self.sBeginSpec, self.sEndSpec, self.sEndOutput = val.split(' ') 

-

except ValueError: 

-

raise CogUsageError( 

-

'--markers requires 3 values separated by spaces, could not parse %r' % val 

-

) 

-

 

-

def validate(self): 

-

""" Does nothing if everything is OK, raises CogError's if it's not. 

-

""" 

-

if self.bReplace and self.bDeleteCode: 

-

raise CogUsageError("Can't use -d with -r (or you would delete all your source!)") 

-

 

-

if self.bReplace and self.sOutputName: 

-

raise CogUsageError("Can't use -o with -r (they are opposites)") 

-

 

-

 

-

class Cog(Redirectable): 

-

""" The Cog engine. 

-

""" 

-

def __init__(self): 

-

Redirectable.__init__(self) 

-

self.options = CogOptions() 

-

self._fixEndOutputPatterns() 

-

self.installCogModule() 

-

 

-

def _fixEndOutputPatterns(self): 

-

end_output = re.escape(self.options.sEndOutput) 

-

self.reEndOutput = re.compile(end_output + r'(?P<hashsect> *\(checksum: (?P<hash>[a-f0-9]+)\))') 

-

self.sEndFormat = self.options.sEndOutput + ' (checksum: %s)' 

-

 

-

def showWarning(self, msg): 

-

self.prout("Warning: "+msg) 

-

 

-

def isBeginSpecLine(self, s): 

-

return self.options.sBeginSpec in s 

-

 

-

def isEndSpecLine(self, s): 

-

return self.options.sEndSpec in s and not self.isEndOutputLine(s) 

-

 

-

def isEndOutputLine(self, s): 

-

return self.options.sEndOutput in s 

-

 

-

def installCogModule(self): 

-

""" Magic mumbo-jumbo so that imported Python modules 

-

can say "import cog" and get our state. 

-

""" 

-

self.cogmodule = imp.new_module('cog') 

-

self.cogmodule.path = [] 

-

sys.modules['cog'] = self.cogmodule 

-

 

-

def openOutputFile(self, fname): 

-

""" Open an output file, taking all the details into account. 

-

""" 

-

opts = {} 

-

mode = "w" 

-

360 ↛ 361line 360 didn't jump to line 361, because the condition on line 360 was never true if PY3: 

-

opts['encoding'] = self.options.sEncoding 

-

if self.options.bNewlines: 

-

363 ↛ 364line 363 didn't jump to line 364, because the condition on line 363 was never true if PY3: 

-

opts['newline'] = "\n" 

-

else: 

-

mode = "wb" 

-

fdir = os.path.dirname(fname) 

-

if os.path.dirname(fdir) and not os.path.exists(fdir): 

-

os.makedirs(fdir) 

-

return open(fname, mode, **opts) 

-

 

-

def openInputFile(self, fname): 

-

""" Open an input file. """ 

-

if fname == "-": 

-

return sys.stdin 

-

else: 

-

opts = {} 

-

378 ↛ 379line 378 didn't jump to line 379, because the condition on line 378 was never true if PY3: 

-

opts['encoding'] = self.options.sEncoding 

-

return open(fname, "r", **opts) 

-

 

-

def processFile(self, fIn, fOut, fname=None, globals=None): 

-

""" Process an input file object to an output file object. 

-

fIn and fOut can be file objects, or file names. 

-

""" 

-

 

-

sFileIn = fname or '' 

-

sFileOut = fname or '' 

-

fInToClose = fOutToClose = None 

-

# Convert filenames to files. 

-

if isinstance(fIn, string_types): 

-

# Open the input file. 

-

sFileIn = fIn 

-

fIn = fInToClose = self.openInputFile(fIn) 

-

if isinstance(fOut, string_types): 

-

# Open the output file. 

-

sFileOut = fOut 

-

fOut = fOutToClose = self.openOutputFile(fOut) 

-

 

-

try: 

-

fIn = NumberedFileReader(fIn) 

-

 

-

bSawCog = False 

-

 

-

self.cogmodule.inFile = sFileIn 

-

self.cogmodule.outFile = sFileOut 

-

 

-

# The globals dict we'll use for this file. 

-

if globals is None: 

-

globals = {} 

-

 

-

# If there are any global defines, put them in the globals. 

-

globals.update(self.options.defines) 

-

 

-

# loop over generator chunks 

-

l = fIn.readline() 

-

while l: 

-

# Find the next spec begin 

-

while l and not self.isBeginSpecLine(l): 

-

if self.isEndSpecLine(l): 

-

raise CogError("Unexpected '%s'" % self.options.sEndSpec, 

-

file=sFileIn, line=fIn.linenumber()) 

-

if self.isEndOutputLine(l): 

-

raise CogError("Unexpected '%s'" % self.options.sEndOutput, 

-

file=sFileIn, line=fIn.linenumber()) 

-

fOut.write(l) 

-

l = fIn.readline() 

-

if not l: 

-

break 

-

if not self.options.bDeleteCode: 

-

fOut.write(l) 

-

 

-

# l is the begin spec 

-

gen = CogGenerator() 

-

gen.setOutput(stdout=self.stdout) 

-

gen.parseMarker(l) 

-

firstLineNum = fIn.linenumber() 

-

self.cogmodule.firstLineNum = firstLineNum 

-

 

-

# If the spec begin is also a spec end, then process the single 

-

# line of code inside. 

-

if self.isEndSpecLine(l): 

-

beg = l.find(self.options.sBeginSpec) 

-

end = l.find(self.options.sEndSpec) 

-

if beg > end: 

-

raise CogError("Cog code markers inverted", 

-

file=sFileIn, line=firstLineNum) 

-

else: 

-

sCode = l[beg+len(self.options.sBeginSpec):end].strip() 

-

gen.parseLine(sCode) 

-

else: 

-

# Deal with an ordinary code block. 

-

l = fIn.readline() 

-

 

-

# Get all the lines in the spec 

-

while l and not self.isEndSpecLine(l): 

-

if self.isBeginSpecLine(l): 

-

raise CogError("Unexpected '%s'" % self.options.sBeginSpec, 

-

file=sFileIn, line=fIn.linenumber()) 

-

if self.isEndOutputLine(l): 

-

raise CogError("Unexpected '%s'" % self.options.sEndOutput, 

-

file=sFileIn, line=fIn.linenumber()) 

-

if not self.options.bDeleteCode: 

-

fOut.write(l) 

-

gen.parseLine(l) 

-

l = fIn.readline() 

-

if not l: 

-

raise CogError( 

-

"Cog block begun but never ended.", 

-

file=sFileIn, line=firstLineNum) 

-

 

-

if not self.options.bDeleteCode: 

-

fOut.write(l) 

-

gen.parseMarker(l) 

-

 

-

l = fIn.readline() 

-

 

-

# Eat all the lines in the output section. While reading past 

-

# them, compute the md5 hash of the old output. 

-

previous = "" 

-

hasher = hashlib.md5() 

-

while l and not self.isEndOutputLine(l): 

-

if self.isBeginSpecLine(l): 

-

raise CogError("Unexpected '%s'" % self.options.sBeginSpec, 

-

file=sFileIn, line=fIn.linenumber()) 

-

if self.isEndSpecLine(l): 

-

raise CogError("Unexpected '%s'" % self.options.sEndSpec, 

-

file=sFileIn, line=fIn.linenumber()) 

-

previous += l 

-

hasher.update(to_bytes(l)) 

-

l = fIn.readline() 

-

curHash = hasher.hexdigest() 

-

 

-

if not l and not self.options.bEofCanBeEnd: 

-

# We reached end of file before we found the end output line. 

-

raise CogError("Missing '%s' before end of file." % self.options.sEndOutput, 

-

file=sFileIn, line=fIn.linenumber()) 

-

 

-

# Make the previous output available to the current code 

-

self.cogmodule.previous = previous 

-

 

-

# Write the output of the spec to be the new output if we're 

-

# supposed to generate code. 

-

hasher = hashlib.md5() 

-

if not self.options.bNoGenerate: 

-

sFile = "%s+%d" % (sFileIn, firstLineNum) 

-

sGen = gen.evaluate(cog=self, globals=globals, fname=sFile) 

-

sGen = self.suffixLines(sGen) 

-

hasher.update(to_bytes(sGen)) 

-

fOut.write(sGen) 

-

newHash = hasher.hexdigest() 

-

 

-

bSawCog = True 

-

 

-

# Write the ending output line 

-

hashMatch = self.reEndOutput.search(l) 

-

if self.options.bHashOutput: 

-

if hashMatch: 

-

oldHash = hashMatch.groupdict()['hash'] 

-

if oldHash != curHash: 

-

raise CogError("Output has been edited! Delete old checksum to unprotect.", 

-

file=sFileIn, line=fIn.linenumber()) 

-

# Create a new end line with the correct hash. 

-

endpieces = l.split(hashMatch.group(0), 1) 

-

else: 

-

# There was no old hash, but we want a new hash. 

-

endpieces = l.split(self.options.sEndOutput, 1) 

-

l = (self.sEndFormat % newHash).join(endpieces) 

-

else: 

-

# We don't want hashes output, so if there was one, get rid of 

-

# it. 

-

if hashMatch: 

-

l = l.replace(hashMatch.groupdict()['hashsect'], '', 1) 

-

 

-

if not self.options.bDeleteCode: 

-

fOut.write(l) 

-

l = fIn.readline() 

-

 

-

if not bSawCog and self.options.bWarnEmpty: 

-

self.showWarning("no cog code found in %s" % sFileIn) 

-

finally: 

-

if fInToClose: 

-

fInToClose.close() 

-

if fOutToClose: 

-

fOutToClose.close() 

-

 

-

 

-

# A regex for non-empty lines, used by suffixLines. 

-

reNonEmptyLines = re.compile("^\s*\S+.*$", re.MULTILINE) 

-

 

-

def suffixLines(self, text): 

-

""" Add suffixes to the lines in text, if our options desire it. 

-

text is many lines, as a single string. 

-

""" 

-

if self.options.sSuffix: 

-

# Find all non-blank lines, and add the suffix to the end. 

-

repl = r"\g<0>" + self.options.sSuffix.replace('\\', '\\\\') 

-

text = self.reNonEmptyLines.sub(repl, text) 

-

return text 

-

 

-

def processString(self, sInput, fname=None): 

-

""" Process sInput as the text to cog. 

-

Return the cogged output as a string. 

-

""" 

-

fOld = StringIO(sInput) 

-

fNew = StringIO() 

-

self.processFile(fOld, fNew, fname=fname) 

-

return fNew.getvalue() 

-

 

-

def replaceFile(self, sOldPath, sNewText): 

-

""" Replace file sOldPath with the contents sNewText 

-

""" 

-

if not os.access(sOldPath, os.W_OK): 

-

# Need to ensure we can write. 

-

if self.options.sMakeWritableCmd: 

-

# Use an external command to make the file writable. 

-

cmd = self.options.sMakeWritableCmd.replace('%s', sOldPath) 

-

self.stdout.write(os.popen(cmd).read()) 

-

if not os.access(sOldPath, os.W_OK): 

-

raise CogError("Couldn't make %s writable" % sOldPath) 

-

else: 

-

# Can't write! 

-

raise CogError("Can't overwrite %s" % sOldPath) 

-

f = self.openOutputFile(sOldPath) 

-

f.write(sNewText) 

-

f.close() 

-

 

-

def saveIncludePath(self): 

-

self.savedInclude = self.options.includePath[:] 

-

self.savedSysPath = sys.path[:] 

-

 

-

def restoreIncludePath(self): 

-

self.options.includePath = self.savedInclude 

-

self.cogmodule.path = self.options.includePath 

-

sys.path = self.savedSysPath 

-

 

-

def addToIncludePath(self, includePath): 

-

self.cogmodule.path.extend(includePath) 

-

sys.path.extend(includePath) 

-

 

-

def processOneFile(self, sFile): 

-

""" Process one filename through cog. 

-

""" 

-

 

-

self.saveIncludePath() 

-

bNeedNewline = False 

-

 

-

try: 

-

self.addToIncludePath(self.options.includePath) 

-

# Since we know where the input file came from, 

-

# push its directory onto the include path. 

-

self.addToIncludePath([os.path.dirname(sFile)]) 

-

 

-

# How we process the file depends on where the output is going. 

-

if self.options.sOutputName: 

-

self.processFile(sFile, self.options.sOutputName, sFile) 

-

elif self.options.bReplace: 

-

# We want to replace the cog file with the output, 

-

# but only if they differ. 

-

if self.options.verbosity >= 2: 

-

self.prout("Cogging %s" % sFile, end="") 

-

bNeedNewline = True 

-

 

-

try: 

-

fOldFile = self.openInputFile(sFile) 

-

sOldText = fOldFile.read() 

-

fOldFile.close() 

-

sNewText = self.processString(sOldText, fname=sFile) 

-

if sOldText != sNewText: 

-

if self.options.verbosity >= 1: 

-

if self.options.verbosity < 2: 

-

self.prout("Cogging %s" % sFile, end="") 

-

self.prout(" (changed)") 

-

bNeedNewline = False 

-

self.replaceFile(sFile, sNewText) 

-

finally: 

-

# The try-finally block is so we can print a partial line 

-

# with the name of the file, and print (changed) on the 

-

# same line, but also make sure to break the line before 

-

# any traceback. 

-

if bNeedNewline: 

-

self.prout("") 

-

else: 

-

self.processFile(sFile, self.stdout, sFile) 

-

finally: 

-

self.restoreIncludePath() 

-

 

-

def processFileList(self, sFileList): 

-

""" Process the files in a file list. 

-

""" 

-

flist = self.openInputFile(sFileList) 

-

lines = flist.readlines() 

-

flist.close() 

-

for l in lines: 

-

# Use shlex to parse the line like a shell. 

-

lex = shlex.shlex(l, posix=True) 

-

lex.whitespace_split = True 

-

lex.commenters = '#' 

-

# No escapes, so that backslash can be part of the path 

-

lex.escape = '' 

-

args = list(lex) 

-

if args: 

-

self.processArguments(args) 

-

 

-

def processArguments(self, args): 

-

""" Process one command-line. 

-

""" 

-

saved_options = self.options 

-

self.options = self.options.clone() 

-

 

-

self.options.parseArgs(args[1:]) 

-

self.options.validate() 

-

 

-

if args[0][0] == '@': 

-

if self.options.sOutputName: 

-

raise CogUsageError("Can't use -o with @file") 

-

self.processFileList(args[0][1:]) 

-

else: 

-

self.processOneFile(args[0]) 

-

 

-

self.options = saved_options 

-

 

-

def callableMain(self, argv): 

-

""" All of command-line cog, but in a callable form. 

-

This is used by main. 

-

argv is the equivalent of sys.argv. 

-

""" 

-

argv = argv[1:] 

-

 

-

# Provide help if asked for anywhere in the command line. 

-

if '-?' in argv or '-h' in argv: 

-

self.prerr(usage, end="") 

-

return 

-

 

-

self.options.parseArgs(argv) 

-

self.options.validate() 

-

self._fixEndOutputPatterns() 

-

 

-

if self.options.bShowVersion: 

-

self.prout("Cog version %s" % __version__) 

-

return 

-

 

-

if self.options.args: 

-

for a in self.options.args: 

-

self.processArguments([a]) 

-

else: 

-

raise CogUsageError("No files to process") 

-

 

-

def main(self, argv): 

-

""" Handle the command-line execution for cog. 

-

""" 

-

 

-

try: 

-

self.callableMain(argv) 

-

return 0 

-

except CogUsageError as err: 

-

self.prerr(err) 

-

self.prerr("(for help use -?)") 

-

return 2 

-

except CogGeneratedError as err: 

-

self.prerr("Error: %s" % err) 

-

return 3 

-

except CogError as err: 

-

self.prerr(err) 

-

return 1 

-

except: 

-

traceback.print_exc(None, self.stderr) 

-

return 1 

-

 

-

# History: 

-

# 20040210: First public version. 

-

# 20040220: Text preceding the start and end marker are removed from Python lines. 

-

# -v option on the command line shows the version. 

-

# 20040311: Make sure the last line of output is properly ended with a newline. 

-

# 20040605: Fixed some blank line handling in cog. 

-

# Fixed problems with assigning to xml elements in handyxml. 

-

# 20040621: Changed all line-ends to LF from CRLF. 

-

# 20041002: Refactor some option handling to simplify unittesting the options. 

-

# 20041118: cog.out and cog.outl have optional string arguments. 

-

# 20041119: File names weren't being properly passed around for warnings, etc. 

-

# 20041122: Added cog.firstLineNum: a property with the line number of the [[[cog line. 

-

# Added cog.inFile and cog.outFile: the names of the input and output file. 

-

# 20041218: Single-line cog generators, with start marker and end marker on 

-

# the same line. 

-

# 20041230: Keep a single globals dict for all the code fragments in a single 

-

# file so they can share state. 

-

# 20050206: Added the -x switch to remove all generated output. 

-

# 20050218: Now code can be on the marker lines as well. 

-

# 20050219: Added -c switch to checksum the output so that edits can be 

-

# detected before they are obliterated. 

-

# 20050521: Added cog.error, contributed by Alexander Belchenko. 

-

# 20050720: Added code deletion and settable globals contributed by Blake Winton. 

-

# 20050724: Many tweaks to improve code coverage. 

-

# 20050726: Error messages are now printed with no traceback. 

-

# Code can no longer appear on the marker lines, 

-

# except for single-line style. 

-

# -z allows omission of the [[[end]]] marker, and it will be assumed 

-

# at the end of the file. 

-

# 20050729: Refactor option parsing into a separate class, in preparation for 

-

# future features. 

-

# 20050805: The cogmodule.path wasn't being properly maintained. 

-

# 20050808: Added the -D option to define a global value. 

-

# 20050810: The %s in the -w command is dealt with more robustly. 

-

# Added the -s option to suffix output lines with a marker. 

-

# 20050817: Now @files can have arguments on each line to change the cog's 

-

# behavior for that line. 

-

# 20051006: Version 2.0 

-

# 20080521: -U options lets you create Unix newlines on Windows. Thanks, 

-

# Alexander Belchenko. 

-

# 20080522: It's now ok to have -d with output to stdout, and now we validate 

-

# the args after each line of an @file. 

-

# 20090520: Use hashlib where it's available, to avoid a warning. 

-

# Use the builtin compile() instead of compiler, for Jython. 

-

# Explicitly close files we opened, Jython likes this. 

-

# 20120205: Port to Python 3. Lowest supported version is 2.6. 

-

# 20150104: --markers option added by Doug Hellmann. 

-

# 20150104: -n ENCODING option added by Petr Gladkiy. 

-

# 20150107: Added --verbose to control what files get listed. 

-

# 20150111: Version 2.4 

-

# 20160213: v2.5: -o makes needed directories, thanks Jean-François Giraud. 

-

# 20161019: Added a LICENSE.txt file. 

- -
+

1# coding: utf8 

+

2""" Cog content generation tool. 

+

3 http://nedbatchelder.com/code/cog 

+

4 

+

5 Copyright 2004-2019, Ned Batchelder. 

+

6""" 

+

7 

+

8from __future__ import absolute_import, print_function 

+

9 

+

10import copy 

+

11import getopt 

+

12import glob 

+

13import hashlib 

+

14import imp 

+

15import linecache 

+

16import os 

+

17import re 

+

18import shlex 

+

19import sys 

+

20import traceback 

+

21 

+

22from .backward import PY3, StringIO, string_types, to_bytes 

+

23 

+

24__all__ = ['Cog', 'CogUsageError', 'main'] 

+

25 

+

26__version__ = '3.0.0' 

+

27 

+

28usage = """\ 

+

29cog - generate content with inlined Python code. 

+

30 

+

31cog [OPTIONS] [INFILE | @FILELIST] ... 

+

32 

+

33INFILE is the name of an input file, '-' will read from stdin. 

+

34FILELIST is the name of a text file containing file names or 

+

35 other @FILELISTs. 

+

36 

+

37OPTIONS: 

+

38 -c Checksum the output to protect it against accidental change. 

+

39 -d Delete the generator code from the output file. 

+

40 -D name=val Define a global string available to your generator code. 

+

41 -e Warn if a file has no cog code in it. 

+

42 -I PATH Add PATH to the list of directories for data files and modules. 

+

43 -n ENCODING Use ENCODING when reading and writing files. 

+

44 -o OUTNAME Write the output to OUTNAME. 

+

45 -p PROLOGUE Prepend the generator source with PROLOGUE. Useful to insert an 

+

46 import line. Example: -p "import math" 

+

47 -r Replace the input file with the output. 

+

48 -s STRING Suffix all generated output lines with STRING. 

+

49 -U Write the output with Unix newlines (only LF line-endings). 

+

50 -w CMD Use CMD if the output file needs to be made writable. 

+

51 A %s in the CMD will be filled with the filename. 

+

52 -x Excise all the generated output without running the generators. 

+

53 -z The end-output marker can be omitted, and is assumed at eof. 

+

54 -v Print the version of cog and exit. 

+

55 --verbosity=VERBOSITY 

+

56 Control the amount of output. 2 (the default) lists all files, 

+

57 1 lists only changed files, 0 lists no files. 

+

58 --markers='START END END-OUTPUT' 

+

59 The patterns surrounding cog inline instructions. Should 

+

60 include three values separated by spaces, the start, end, 

+

61 and end-output markers. Defaults to '[[[cog ]]] [[[end]]]'. 

+

62 -h Print this help. 

+

63""" 

+

64 

+

65# Other package modules 

+

66from .whiteutils import * 

+

67 

+

68class CogError(Exception): 

+

69 """ Any exception raised by Cog. 

+

70 """ 

+

71 def __init__(self, msg, file='', line=0): 

+

72 if file: 

+

73 Exception.__init__(self, "%s(%d): %s" % (file, line, msg)) 

+

74 else: 

+

75 Exception.__init__(self, msg) 

+

76 

+

77class CogUsageError(CogError): 

+

78 """ An error in usage of command-line arguments in cog. 

+

79 """ 

+

80 pass 

+

81 

+

82class CogInternalError(CogError): 

+

83 """ An error in the coding of Cog. Should never happen. 

+

84 """ 

+

85 pass 

+

86 

+

87class CogGeneratedError(CogError): 

+

88 """ An error raised by a user's cog generator. 

+

89 """ 

+

90 pass 

+

91 

+

92class CogUserException(CogError): 

+

93 """ An exception caught when running a user's cog generator. 

+

94 The argument is the traceback message to print. 

+

95 """ 

+

96 pass 

+

97 

+

98class Redirectable: 

+

99 """ An object with its own stdout and stderr files. 

+

100 """ 

+

101 def __init__(self): 

+

102 self.stdout = sys.stdout 

+

103 self.stderr = sys.stderr 

+

104 

+

105 def setOutput(self, stdout=None, stderr=None): 

+

106 """ Assign new files for standard out and/or standard error. 

+

107 """ 

+

108 if stdout: 108 ↛ 110line 108 didn't jump to line 110, because the condition on line 108 was never false

+

109 self.stdout = stdout 

+

110 if stderr: 110 ↛ 111line 110 didn't jump to line 111, because the condition on line 110 was never true

+

111 self.stderr = stderr 

+

112 

+

113 def prout(self, s, end="\n"): 

+

114 print(s, file=self.stdout, end=end) 

+

115 

+

116 def prerr(self, s, end="\n"): 

+

117 print(s, file=self.stderr, end=end) 

+

118 

+

119 

+

120class CogGenerator(Redirectable): 

+

121 """ A generator pulled from a source file. 

+

122 """ 

+

123 def __init__(self, options=None): 

+

124 Redirectable.__init__(self) 

+

125 self.markers = [] 

+

126 self.lines = [] 

+

127 self.options = options or CogOptions() 

+

128 

+

129 def parseMarker(self, l): 

+

130 self.markers.append(l) 

+

131 

+

132 def parseLine(self, l): 

+

133 self.lines.append(l.strip('\n')) 

+

134 

+

135 def getCode(self): 

+

136 """ Extract the executable Python code from the generator. 

+

137 """ 

+

138 # If the markers and lines all have the same prefix 

+

139 # (end-of-line comment chars, for example), 

+

140 # then remove it from all the lines. 

+

141 prefIn = commonPrefix(self.markers + self.lines) 

+

142 if prefIn: 

+

143 self.markers = [ l.replace(prefIn, '', 1) for l in self.markers ] 

+

144 self.lines = [ l.replace(prefIn, '', 1) for l in self.lines ] 

+

145 

+

146 return reindentBlock(self.lines, '') 

+

147 

+

148 def evaluate(self, cog, globals, fname): 

+

149 # figure out the right whitespace prefix for the output 

+

150 prefOut = whitePrefix(self.markers) 

+

151 

+

152 intext = self.getCode() 

+

153 if not intext: 

+

154 return '' 

+

155 

+

156 prologue = "import " + cog.cogmodulename + " as cog\n" 

+

157 if self.options.sPrologue: 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true

+

158 prologue += self.options.sPrologue + '\n' 

+

159 code = compile(prologue + intext, str(fname), 'exec') 

+

160 

+

161 # Make sure the "cog" module has our state. 

+

162 cog.cogmodule.msg = self.msg 

+

163 cog.cogmodule.out = self.out 

+

164 cog.cogmodule.outl = self.outl 

+

165 cog.cogmodule.error = self.error 

+

166 

+

167 self.outstring = '' 

+

168 try: 

+

169 eval(code, globals) 

+

170 except CogError: 170 ↛ 171line 170 didn't jump to line 171, because the exception caught by line 170 didn't happen

+

171 raise 

+

172 except: 

+

173 typ, err, tb = sys.exc_info() 

+

174 frames = (tuple(fr) for fr in traceback.extract_tb(tb.tb_next)) 

+

175 frames = find_cog_source(frames, prologue) 

+

176 msg = "".join(traceback.format_list(frames)) 

+

177 msg += "{}: {}".format(typ.__name__, err) 

+

178 raise CogUserException(msg) 

+

179 

+

180 # We need to make sure that the last line in the output 

+

181 # ends with a newline, or it will be joined to the 

+

182 # end-output line, ruining cog's idempotency. 

+

183 if self.outstring and self.outstring[-1] != '\n': 

+

184 self.outstring += '\n' 

+

185 

+

186 return reindentBlock(self.outstring, prefOut) 

+

187 

+

188 def msg(self, s): 

+

189 self.prout("Message: "+s) 

+

190 

+

191 def out(self, sOut='', dedent=False, trimblanklines=False): 

+

192 """ The cog.out function. 

+

193 """ 

+

194 if trimblanklines and ('\n' in sOut): 

+

195 lines = sOut.split('\n') 

+

196 if lines[0].strip() == '': 

+

197 del lines[0] 

+

198 if lines and lines[-1].strip() == '': 

+

199 del lines[-1] 

+

200 sOut = '\n'.join(lines)+'\n' 

+

201 if dedent: 

+

202 sOut = reindentBlock(sOut) 

+

203 self.outstring += sOut 

+

204 

+

205 def outl(self, sOut='', **kw): 

+

206 """ The cog.outl function. 

+

207 """ 

+

208 self.out(sOut, **kw) 

+

209 self.out('\n') 

+

210 

+

211 def error(self, msg='Error raised by cog generator.'): 

+

212 """ The cog.error function. 

+

213 Instead of raising standard python errors, cog generators can use 

+

214 this function. It will display the error without a scary Python 

+

215 traceback. 

+

216 """ 

+

217 raise CogGeneratedError(msg) 

+

218 

+

219 

+

220class NumberedFileReader: 

+

221 """ A decorator for files that counts the readline()'s called. 

+

222 """ 

+

223 def __init__(self, f): 

+

224 self.f = f 

+

225 self.n = 0 

+

226 

+

227 def readline(self): 

+

228 l = self.f.readline() 

+

229 if l: 

+

230 self.n += 1 

+

231 return l 

+

232 

+

233 def linenumber(self): 

+

234 return self.n 

+

235 

+

236 

+

237class CogOptions: 

+

238 """ Options for a run of cog. 

+

239 """ 

+

240 def __init__(self): 

+

241 # Defaults for argument values. 

+

242 self.args = [] 

+

243 self.includePath = [] 

+

244 self.defines = {} 

+

245 self.bShowVersion = False 

+

246 self.sMakeWritableCmd = None 

+

247 self.bReplace = False 

+

248 self.bNoGenerate = False 

+

249 self.sOutputName = None 

+

250 self.bWarnEmpty = False 

+

251 self.bHashOutput = False 

+

252 self.bDeleteCode = False 

+

253 self.bEofCanBeEnd = False 

+

254 self.sSuffix = None 

+

255 self.bNewlines = False 

+

256 self.sBeginSpec = '[[[cog' 

+

257 self.sEndSpec = ']]]' 

+

258 self.sEndOutput = '[[[end]]]' 

+

259 self.sEncoding = "utf-8" 

+

260 self.verbosity = 2 

+

261 self.sPrologue = '' 

+

262 

+

263 def __eq__(self, other): 

+

264 """ Comparison operator for tests to use. 

+

265 """ 

+

266 return self.__dict__ == other.__dict__ 

+

267 

+

268 def clone(self): 

+

269 """ Make a clone of these options, for further refinement. 

+

270 """ 

+

271 return copy.deepcopy(self) 

+

272 

+

273 def addToIncludePath(self, dirs): 

+

274 """ Add directories to the include path. 

+

275 """ 

+

276 dirs = dirs.split(os.pathsep) 

+

277 self.includePath.extend(dirs) 

+

278 

+

279 def parseArgs(self, argv): 

+

280 # Parse the command line arguments. 

+

281 try: 

+

282 opts, self.args = getopt.getopt( 

+

283 argv, 

+

284 'cdD:eI:n:o:rs:p:Uvw:xz', 

+

285 [ 

+

286 'markers=', 

+

287 'verbosity=', 

+

288 ] 

+

289 ) 

+

290 except getopt.error as msg: 

+

291 raise CogUsageError(msg) 

+

292 

+

293 # Handle the command line arguments. 

+

294 for o, a in opts: 

+

295 if o == '-c': 

+

296 self.bHashOutput = True 

+

297 elif o == '-d': 

+

298 self.bDeleteCode = True 

+

299 elif o == '-D': 

+

300 if a.count('=') < 1: 

+

301 raise CogUsageError("-D takes a name=value argument") 

+

302 name, value = a.split('=', 1) 

+

303 self.defines[name] = value 

+

304 elif o == '-e': 

+

305 self.bWarnEmpty = True 

+

306 elif o == '-I': 

+

307 self.addToIncludePath(a) 

+

308 elif o == '-n': 

+

309 self.sEncoding = a 

+

310 elif o == '-o': 

+

311 self.sOutputName = a 

+

312 elif o == '-r': 

+

313 self.bReplace = True 

+

314 elif o == '-s': 

+

315 self.sSuffix = a 

+

316 elif o == '-p': 

+

317 self.sPrologue = a 

+

318 elif o == '-U': 

+

319 self.bNewlines = True 

+

320 elif o == '-v': 

+

321 self.bShowVersion = True 

+

322 elif o == '-w': 

+

323 self.sMakeWritableCmd = a 

+

324 elif o == '-x': 

+

325 self.bNoGenerate = True 

+

326 elif o == '-z': 

+

327 self.bEofCanBeEnd = True 

+

328 elif o == '--markers': 

+

329 self._parse_markers(a) 

+

330 elif o == '--verbosity': 

+

331 self.verbosity = int(a) 

+

332 else: 

+

333 # Since getopt.getopt is given a list of possible flags, 

+

334 # this is an internal error. 

+

335 raise CogInternalError("Don't understand argument %s" % o) 

+

336 

+

337 def _parse_markers(self, val): 

+

338 try: 

+

339 self.sBeginSpec, self.sEndSpec, self.sEndOutput = val.split(' ') 

+

340 except ValueError: 

+

341 raise CogUsageError( 

+

342 '--markers requires 3 values separated by spaces, could not parse %r' % val 

+

343 ) 

+

344 

+

345 def validate(self): 

+

346 """ Does nothing if everything is OK, raises CogError's if it's not. 

+

347 """ 

+

348 if self.bReplace and self.bDeleteCode: 

+

349 raise CogUsageError("Can't use -d with -r (or you would delete all your source!)") 

+

350 

+

351 if self.bReplace and self.sOutputName: 

+

352 raise CogUsageError("Can't use -o with -r (they are opposites)") 

+

353 

+

354 

+

355class Cog(Redirectable): 

+

356 """ The Cog engine. 

+

357 """ 

+

358 def __init__(self): 

+

359 Redirectable.__init__(self) 

+

360 self.options = CogOptions() 

+

361 self._fixEndOutputPatterns() 

+

362 self.cogmodulename = "cog" 

+

363 self.installCogModule() 

+

364 

+

365 def _fixEndOutputPatterns(self): 

+

366 end_output = re.escape(self.options.sEndOutput) 

+

367 self.reEndOutput = re.compile(end_output + r'(?P<hashsect> *\(checksum: (?P<hash>[a-f0-9]+)\))') 

+

368 self.sEndFormat = self.options.sEndOutput + ' (checksum: %s)' 

+

369 

+

370 def showWarning(self, msg): 

+

371 self.prout("Warning: "+msg) 

+

372 

+

373 def isBeginSpecLine(self, s): 

+

374 return self.options.sBeginSpec in s 

+

375 

+

376 def isEndSpecLine(self, s): 

+

377 return self.options.sEndSpec in s and not self.isEndOutputLine(s) 

+

378 

+

379 def isEndOutputLine(self, s): 

+

380 return self.options.sEndOutput in s 

+

381 

+

382 def installCogModule(self): 

+

383 """ Magic mumbo-jumbo so that imported Python modules 

+

384 can say "import cog" and get our state. 

+

385 """ 

+

386 self.cogmodule = imp.new_module('cog') 

+

387 self.cogmodule.path = [] 

+

388 

+

389 def openOutputFile(self, fname): 

+

390 """ Open an output file, taking all the details into account. 

+

391 """ 

+

392 opts = {} 

+

393 mode = "w" 

+

394 if PY3: 

+

395 opts['encoding'] = self.options.sEncoding 

+

396 if self.options.bNewlines: 

+

397 if PY3: 

+

398 opts['newline'] = "\n" 

+

399 else: 

+

400 mode = "wb" 

+

401 fdir = os.path.dirname(fname) 

+

402 if os.path.dirname(fdir) and not os.path.exists(fdir): 

+

403 os.makedirs(fdir) 

+

404 return open(fname, mode, **opts) 

+

405 

+

406 def openInputFile(self, fname): 

+

407 """ Open an input file. """ 

+

408 if fname == "-": 

+

409 return sys.stdin 

+

410 else: 

+

411 opts = {} 

+

412 if PY3: 

+

413 opts['encoding'] = self.options.sEncoding 

+

414 return open(fname, "r", **opts) 

+

415 

+

416 def processFile(self, fIn, fOut, fname=None, globals=None): 

+

417 """ Process an input file object to an output file object. 

+

418 fIn and fOut can be file objects, or file names. 

+

419 """ 

+

420 

+

421 sFileIn = fname or '' 

+

422 sFileOut = fname or '' 

+

423 fInToClose = fOutToClose = None 

+

424 # Convert filenames to files. 

+

425 if isinstance(fIn, string_types): 425 ↛ 427line 425 didn't jump to line 427, because the condition on line 425 was never true

+

426 # Open the input file. 

+

427 sFileIn = fIn 

+

428 fIn = fInToClose = self.openInputFile(fIn) 

+

429 if isinstance(fOut, string_types): 429 ↛ 431line 429 didn't jump to line 431, because the condition on line 429 was never true

+

430 # Open the output file. 

+

431 sFileOut = fOut 

+

432 fOut = fOutToClose = self.openOutputFile(fOut) 

+

433 

+

434 try: 

+

435 fIn = NumberedFileReader(fIn) 

+

436 

+

437 bSawCog = False 

+

438 

+

439 self.cogmodule.inFile = sFileIn 

+

440 self.cogmodule.outFile = sFileOut 

+

441 self.cogmodulename = 'cog_' + hashlib.md5(sFileOut.encode()).hexdigest() 

+

442 sys.modules[self.cogmodulename] = self.cogmodule 

+

443 # if "import cog" explicitly done in code by user, note threading will cause clashes.  

+

444 sys.modules['cog'] = self.cogmodule 

+

445 

+

446 # The globals dict we'll use for this file. 

+

447 if globals is None: 447 ↛ 451line 447 didn't jump to line 451, because the condition on line 447 was never false

+

448 globals = {} 

+

449 

+

450 # If there are any global defines, put them in the globals. 

+

451 globals.update(self.options.defines) 

+

452 

+

453 # loop over generator chunks 

+

454 l = fIn.readline() 

+

455 while l: 

+

456 # Find the next spec begin 

+

457 while l and not self.isBeginSpecLine(l): 

+

458 if self.isEndSpecLine(l): 458 ↛ 459line 458 didn't jump to line 459, because the condition on line 458 was never true

+

459 raise CogError("Unexpected '%s'" % self.options.sEndSpec, 

+

460 file=sFileIn, line=fIn.linenumber()) 

+

461 if self.isEndOutputLine(l): 461 ↛ 462line 461 didn't jump to line 462, because the condition on line 461 was never true

+

462 raise CogError("Unexpected '%s'" % self.options.sEndOutput, 

+

463 file=sFileIn, line=fIn.linenumber()) 

+

464 fOut.write(l) 

+

465 l = fIn.readline() 

+

466 if not l: 

+

467 break 

+

468 if not self.options.bDeleteCode: 468 ↛ 472line 468 didn't jump to line 472, because the condition on line 468 was never false

+

469 fOut.write(l) 

+

470 

+

471 # l is the begin spec 

+

472 gen = CogGenerator(options=self.options) 

+

473 gen.setOutput(stdout=self.stdout) 

+

474 gen.parseMarker(l) 

+

475 firstLineNum = fIn.linenumber() 

+

476 self.cogmodule.firstLineNum = firstLineNum 

+

477 

+

478 # If the spec begin is also a spec end, then process the single 

+

479 # line of code inside. 

+

480 if self.isEndSpecLine(l): 

+

481 beg = l.find(self.options.sBeginSpec) 

+

482 end = l.find(self.options.sEndSpec) 

+

483 if beg > end: 

+

484 raise CogError("Cog code markers inverted", 

+

485 file=sFileIn, line=firstLineNum) 

+

486 else: 

+

487 sCode = l[beg+len(self.options.sBeginSpec):end].strip() 

+

488 gen.parseLine(sCode) 

+

489 else: 

+

490 # Deal with an ordinary code block. 

+

491 l = fIn.readline() 

+

492 

+

493 # Get all the lines in the spec 

+

494 while l and not self.isEndSpecLine(l): 

+

495 if self.isBeginSpecLine(l): 495 ↛ 496line 495 didn't jump to line 496, because the condition on line 495 was never true

+

496 raise CogError("Unexpected '%s'" % self.options.sBeginSpec, 

+

497 file=sFileIn, line=fIn.linenumber()) 

+

498 if self.isEndOutputLine(l): 498 ↛ 499line 498 didn't jump to line 499, because the condition on line 498 was never true

+

499 raise CogError("Unexpected '%s'" % self.options.sEndOutput, 

+

500 file=sFileIn, line=fIn.linenumber()) 

+

501 if not self.options.bDeleteCode: 501 ↛ 503line 501 didn't jump to line 503, because the condition on line 501 was never false

+

502 fOut.write(l) 

+

503 gen.parseLine(l) 

+

504 l = fIn.readline() 

+

505 if not l: 505 ↛ 506line 505 didn't jump to line 506, because the condition on line 505 was never true

+

506 raise CogError( 

+

507 "Cog block begun but never ended.", 

+

508 file=sFileIn, line=firstLineNum) 

+

509 

+

510 if not self.options.bDeleteCode: 510 ↛ 512line 510 didn't jump to line 512, because the condition on line 510 was never false

+

511 fOut.write(l) 

+

512 gen.parseMarker(l) 

+

513 

+

514 l = fIn.readline() 

+

515 

+

516 # Eat all the lines in the output section. While reading past 

+

517 # them, compute the md5 hash of the old output. 

+

518 previous = "" 

+

519 hasher = hashlib.md5() 

+

520 while l and not self.isEndOutputLine(l): 

+

521 if self.isBeginSpecLine(l): 521 ↛ 522line 521 didn't jump to line 522, because the condition on line 521 was never true

+

522 raise CogError("Unexpected '%s'" % self.options.sBeginSpec, 

+

523 file=sFileIn, line=fIn.linenumber()) 

+

524 if self.isEndSpecLine(l): 524 ↛ 525line 524 didn't jump to line 525, because the condition on line 524 was never true

+

525 raise CogError("Unexpected '%s'" % self.options.sEndSpec, 

+

526 file=sFileIn, line=fIn.linenumber()) 

+

527 previous += l 

+

528 hasher.update(to_bytes(l)) 

+

529 l = fIn.readline() 

+

530 curHash = hasher.hexdigest() 

+

531 

+

532 if not l and not self.options.bEofCanBeEnd: 532 ↛ 534line 532 didn't jump to line 534, because the condition on line 532 was never true

+

533 # We reached end of file before we found the end output line. 

+

534 raise CogError("Missing '%s' before end of file." % self.options.sEndOutput, 

+

535 file=sFileIn, line=fIn.linenumber()) 

+

536 

+

537 # Make the previous output available to the current code 

+

538 self.cogmodule.previous = previous 

+

539 

+

540 # Write the output of the spec to be the new output if we're 

+

541 # supposed to generate code. 

+

542 hasher = hashlib.md5() 

+

543 if not self.options.bNoGenerate: 543 ↛ 549line 543 didn't jump to line 549, because the condition on line 543 was never false

+

544 sFile = "<cog %s:%d>" % (sFileIn, firstLineNum) 

+

545 sGen = gen.evaluate(cog=self, globals=globals, fname=sFile) 

+

546 sGen = self.suffixLines(sGen) 

+

547 hasher.update(to_bytes(sGen)) 

+

548 fOut.write(sGen) 

+

549 newHash = hasher.hexdigest() 

+

550 

+

551 bSawCog = True 

+

552 

+

553 # Write the ending output line 

+

554 hashMatch = self.reEndOutput.search(l) 

+

555 if self.options.bHashOutput: 555 ↛ 556line 555 didn't jump to line 556, because the condition on line 555 was never true

+

556 if hashMatch: 

+

557 oldHash = hashMatch.groupdict()['hash'] 

+

558 if oldHash != curHash: 

+

559 raise CogError("Output has been edited! Delete old checksum to unprotect.", 

+

560 file=sFileIn, line=fIn.linenumber()) 

+

561 # Create a new end line with the correct hash. 

+

562 endpieces = l.split(hashMatch.group(0), 1) 

+

563 else: 

+

564 # There was no old hash, but we want a new hash. 

+

565 endpieces = l.split(self.options.sEndOutput, 1) 

+

566 l = (self.sEndFormat % newHash).join(endpieces) 

+

567 else: 

+

568 # We don't want hashes output, so if there was one, get rid of 

+

569 # it. 

+

570 if hashMatch: 570 ↛ 571line 570 didn't jump to line 571, because the condition on line 570 was never true

+

571 l = l.replace(hashMatch.groupdict()['hashsect'], '', 1) 

+

572 

+

573 if not self.options.bDeleteCode: 573 ↛ 575line 573 didn't jump to line 575, because the condition on line 573 was never false

+

574 fOut.write(l) 

+

575 l = fIn.readline() 

+

576 

+

577 if not bSawCog and self.options.bWarnEmpty: 577 ↛ 578line 577 didn't jump to line 578, because the condition on line 577 was never true

+

578 self.showWarning("no cog code found in %s" % sFileIn) 

+

579 finally: 

+

580 if fInToClose: 580 ↛ 581line 580 didn't jump to line 581, because the condition on line 580 was never true

+

581 fInToClose.close() 

+

582 if fOutToClose: 582 ↛ 583line 582 didn't jump to line 583, because the condition on line 582 was never true

+

583 fOutToClose.close() 

+

584 

+

585 

+

586 # A regex for non-empty lines, used by suffixLines. 

+

587 reNonEmptyLines = re.compile(r"^\s*\S+.*$", re.MULTILINE) 

+

588 

+

589 def suffixLines(self, text): 

+

590 """ Add suffixes to the lines in text, if our options desire it. 

+

591 text is many lines, as a single string. 

+

592 """ 

+

593 if self.options.sSuffix: 593 ↛ 595line 593 didn't jump to line 595, because the condition on line 593 was never true

+

594 # Find all non-blank lines, and add the suffix to the end. 

+

595 repl = r"\g<0>" + self.options.sSuffix.replace('\\', '\\\\') 

+

596 text = self.reNonEmptyLines.sub(repl, text) 

+

597 return text 

+

598 

+

599 def processString(self, sInput, fname=None): 

+

600 """ Process sInput as the text to cog. 

+

601 Return the cogged output as a string. 

+

602 """ 

+

603 fOld = StringIO(sInput) 

+

604 fNew = StringIO() 

+

605 self.processFile(fOld, fNew, fname=fname) 

+

606 return fNew.getvalue() 

+

607 

+

608 def replaceFile(self, sOldPath, sNewText): 

+

609 """ Replace file sOldPath with the contents sNewText 

+

610 """ 

+

611 if not os.access(sOldPath, os.W_OK): 

+

612 # Need to ensure we can write. 

+

613 if self.options.sMakeWritableCmd: 

+

614 # Use an external command to make the file writable. 

+

615 cmd = self.options.sMakeWritableCmd.replace('%s', sOldPath) 

+

616 self.stdout.write(os.popen(cmd).read()) 

+

617 if not os.access(sOldPath, os.W_OK): 

+

618 raise CogError("Couldn't make %s writable" % sOldPath) 

+

619 else: 

+

620 # Can't write! 

+

621 raise CogError("Can't overwrite %s" % sOldPath) 

+

622 f = self.openOutputFile(sOldPath) 

+

623 f.write(sNewText) 

+

624 f.close() 

+

625 

+

626 def saveIncludePath(self): 

+

627 self.savedInclude = self.options.includePath[:] 

+

628 self.savedSysPath = sys.path[:] 

+

629 

+

630 def restoreIncludePath(self): 

+

631 self.options.includePath = self.savedInclude 

+

632 self.cogmodule.path = self.options.includePath 

+

633 sys.path = self.savedSysPath 

+

634 

+

635 def addToIncludePath(self, includePath): 

+

636 self.cogmodule.path.extend(includePath) 

+

637 sys.path.extend(includePath) 

+

638 

+

639 def processOneFile(self, sFile): 

+

640 """ Process one filename through cog. 

+

641 """ 

+

642 

+

643 self.saveIncludePath() 

+

644 bNeedNewline = False 

+

645 

+

646 try: 

+

647 self.addToIncludePath(self.options.includePath) 

+

648 # Since we know where the input file came from, 

+

649 # push its directory onto the include path. 

+

650 self.addToIncludePath([os.path.dirname(sFile)]) 

+

651 

+

652 # How we process the file depends on where the output is going. 

+

653 if self.options.sOutputName: 

+

654 self.processFile(sFile, self.options.sOutputName, sFile) 

+

655 elif self.options.bReplace: 

+

656 # We want to replace the cog file with the output, 

+

657 # but only if they differ. 

+

658 if self.options.verbosity >= 2: 

+

659 self.prout("Cogging %s" % sFile, end="") 

+

660 bNeedNewline = True 

+

661 

+

662 try: 

+

663 fOldFile = self.openInputFile(sFile) 

+

664 sOldText = fOldFile.read() 

+

665 fOldFile.close() 

+

666 sNewText = self.processString(sOldText, fname=sFile) 

+

667 if sOldText != sNewText: 

+

668 if self.options.verbosity >= 1: 

+

669 if self.options.verbosity < 2: 

+

670 self.prout("Cogging %s" % sFile, end="") 

+

671 self.prout(" (changed)") 

+

672 bNeedNewline = False 

+

673 self.replaceFile(sFile, sNewText) 

+

674 finally: 

+

675 # The try-finally block is so we can print a partial line 

+

676 # with the name of the file, and print (changed) on the 

+

677 # same line, but also make sure to break the line before 

+

678 # any traceback. 

+

679 if bNeedNewline: 

+

680 self.prout("") 

+

681 else: 

+

682 self.processFile(sFile, self.stdout, sFile) 

+

683 finally: 

+

684 self.restoreIncludePath() 

+

685 

+

686 def processWildcards(self, sFile): 

+

687 files = glob.glob(sFile) 

+

688 if files: 

+

689 for sMatchingFile in files: 

+

690 self.processOneFile(sMatchingFile) 

+

691 else: 

+

692 self.processOneFile(sFile) 

+

693 

+

694 def processFileList(self, sFileList): 

+

695 """ Process the files in a file list. 

+

696 """ 

+

697 flist = self.openInputFile(sFileList) 

+

698 lines = flist.readlines() 

+

699 flist.close() 

+

700 for l in lines: 

+

701 # Use shlex to parse the line like a shell. 

+

702 lex = shlex.shlex(l, posix=True) 

+

703 lex.whitespace_split = True 

+

704 lex.commenters = '#' 

+

705 # No escapes, so that backslash can be part of the path 

+

706 lex.escape = '' 

+

707 args = list(lex) 

+

708 if args: 

+

709 self.processArguments(args) 

+

710 

+

711 def processArguments(self, args): 

+

712 """ Process one command-line. 

+

713 """ 

+

714 saved_options = self.options 

+

715 self.options = self.options.clone() 

+

716 

+

717 self.options.parseArgs(args[1:]) 

+

718 self.options.validate() 

+

719 

+

720 if args[0][0] == '@': 

+

721 if self.options.sOutputName: 

+

722 raise CogUsageError("Can't use -o with @file") 

+

723 self.processFileList(args[0][1:]) 

+

724 else: 

+

725 self.processWildcards(args[0]) 

+

726 

+

727 self.options = saved_options 

+

728 

+

729 def callableMain(self, argv): 

+

730 """ All of command-line cog, but in a callable form. 

+

731 This is used by main. 

+

732 argv is the equivalent of sys.argv. 

+

733 """ 

+

734 argv = argv[1:] 

+

735 

+

736 # Provide help if asked for anywhere in the command line. 

+

737 if '-?' in argv or '-h' in argv: 

+

738 self.prerr(usage, end="") 

+

739 return 

+

740 

+

741 self.options.parseArgs(argv) 

+

742 self.options.validate() 

+

743 self._fixEndOutputPatterns() 

+

744 

+

745 if self.options.bShowVersion: 

+

746 self.prout("Cog version %s" % __version__) 

+

747 return 

+

748 

+

749 if self.options.args: 

+

750 for a in self.options.args: 

+

751 self.processArguments([a]) 

+

752 else: 

+

753 raise CogUsageError("No files to process") 

+

754 

+

755 def main(self, argv): 

+

756 """ Handle the command-line execution for cog. 

+

757 """ 

+

758 

+

759 try: 

+

760 self.callableMain(argv) 

+

761 return 0 

+

762 except CogUsageError as err: 

+

763 self.prerr(err) 

+

764 self.prerr("(for help use -?)") 

+

765 return 2 

+

766 except CogGeneratedError as err: 

+

767 self.prerr("Error: %s" % err) 

+

768 return 3 

+

769 except CogUserException as err: 

+

770 self.prerr("Traceback (most recent call last):") 

+

771 self.prerr(err.args[0]) 

+

772 return 4 

+

773 except CogError as err: 

+

774 self.prerr(err) 

+

775 return 1 

+

776 

+

777 

+

778def find_cog_source(frame_summary, prologue): 

+

779 """Find cog source lines in a frame summary list, for printing tracebacks. 

+

780 

+

781 Arguments: 

+

782 frame_summary: a list of 4-item tuples, as returned by traceback.extract_tb. 

+

783 prologue: the text of the code prologue. 

+

784 

+

785 Returns 

+

786 A list of 4-item tuples, updated to correct the cog entries. 

+

787 

+

788 """ 

+

789 prolines = prologue.splitlines() 

+

790 for filename, lineno, funcname, source in frame_summary: 

+

791 if not source: 791 ↛ 803line 791 didn't jump to line 803, because the condition on line 791 was never false

+

792 m = re.search(r"^<cog ([^:]+):(\d+)>$", filename) 

+

793 if m: 793 ↛ 794line 793 didn't jump to line 794, because the condition on line 793 was never true

+

794 if lineno <= len(prolines): 

+

795 filename = '<prologue>' 

+

796 source = prolines[lineno-1] 

+

797 lineno -= 1 # Because "import cog" is the first line in the prologue 

+

798 else: 

+

799 filename, coglineno = m.groups() 

+

800 coglineno = int(coglineno) 

+

801 lineno += coglineno - len(prolines) 

+

802 source = linecache.getline(filename, lineno).strip() 

+

803 yield filename, lineno, funcname, source 

+

804 

+

805 

+

806def main(): 

+

807 """Main function for entry_points to use.""" 

+

808 return Cog().main(sys.argv) 

- - diff --git a/doc/sample_html/cogapp_makefiles_py.html b/doc/sample_html/cogapp_makefiles_py.html index 404f24964..4ac84bf19 100644 --- a/doc/sample_html/cogapp_makefiles_py.html +++ b/doc/sample_html/cogapp_makefiles_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/makefiles.py: 92.86% + Coverage for cogapp/makefiles.py: 17.07% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,154 +51,61 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

- -
-

""" Dictionary-to-filetree functions, to create test files for testing. 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2015, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import 

-

import os.path 

-

from .whiteutils import reindentBlock 

-

from .backward import string_types, bytes_types 

-

 

-

__version__ = '1.0.20040126' 

-

__all__ = ['makeFiles', 'removeFiles'] 

-

 

-

def makeFiles(d, basedir='.'): 

-

""" Create files from the dictionary `d`, in the directory named by `basedir`. 

-

""" 

-

for name, contents in d.items(): 

-

child = os.path.join(basedir, name) 

-

if isinstance(contents, string_types): 

-

mode = 'w' 

-

22 ↛ 24line 22 didn't jump to line 24, because the condition on line 22 was never false if isinstance(contents, bytes_types): 

-

mode += "b" 

-

f = open(child, mode) 

-

contents = reindentBlock(contents) 

-

f.write(contents) 

-

f.close() 

-

else: 

-

29 ↛ 31line 29 didn't jump to line 31, because the condition on line 29 was never false if not os.path.exists(child): 

-

os.mkdir(child) 

-

makeFiles(contents, child) 

-

 

-

def removeFiles(d, basedir='.'): 

-

""" Remove the files created by makeFiles. 

-

Directories are removed if they are empty. 

-

""" 

-

for name, contents in d.items(): 

-

child = os.path.join(basedir, name) 

-

if isinstance(contents, string_types): 

-

os.remove(child) 

-

else: 

-

removeFiles(contents, child) 

-

43 ↛ 37line 43 didn't jump to line 37, because the condition on line 43 was never false if not os.listdir(child): 

-

os.rmdir(child) 

-

 

-

if __name__ == '__main__': #pragma: no cover 

-

# Try it a little. 

-

d = { 

-

'test_makefiles': { 

-

'hey.txt': """\ 

-

This is hey.txt. 

-

It's very simple. 

-

""", 

-

'subdir': { 

-

'fooey': """\ 

-

# Fooey 

-

Kablooey 

-

Ew. 

-

""" 

-

} 

-

} 

-

} 

-

makeFiles(d) 

- -
+

1""" Dictionary-to-filetree functions, to create test files for testing. 

+

2 http://nedbatchelder.com/code/cog 

+

3 

+

4 Copyright 2004-2019, Ned Batchelder. 

+

5""" 

+

6 

+

7from __future__ import absolute_import 

+

8 

+

9import os.path 

+

10 

+

11from .backward import string_types, bytes_types 

+

12from .whiteutils import reindentBlock 

+

13 

+

14__all__ = ['makeFiles', 'removeFiles'] 

+

15 

+

16def makeFiles(d, basedir='.', bytes=False): 

+

17 """ Create files from the dictionary `d`, in the directory named by `basedir`. 

+

18 If `bytes` is true, then treat bytestrings as bytes, else as text. 

+

19 """ 

+

20 for name, contents in d.items(): 

+

21 child = os.path.join(basedir, name) 

+

22 if isinstance(contents, string_types): 

+

23 mode = 'w' 

+

24 if bytes and isinstance(contents, bytes_types): 

+

25 mode += "b" 

+

26 f = open(child, mode) 

+

27 contents = reindentBlock(contents) 

+

28 f.write(contents) 

+

29 f.close() 

+

30 else: 

+

31 if not os.path.exists(child): 

+

32 os.mkdir(child) 

+

33 makeFiles(contents, child) 

+

34 

+

35def removeFiles(d, basedir='.'): 

+

36 """ Remove the files created by makeFiles. 

+

37 Directories are removed if they are empty. 

+

38 """ 

+

39 for name, contents in d.items(): 

+

40 child = os.path.join(basedir, name) 

+

41 if isinstance(contents, string_types): 

+

42 os.remove(child) 

+

43 else: 

+

44 removeFiles(contents, child) 

+

45 if not os.listdir(child): 

+

46 os.rmdir(child) 

- - diff --git a/doc/sample_html/cogapp_test_cogapp_py.html b/doc/sample_html/cogapp_test_cogapp_py.html index 5f14aa405..59c6f98c3 100644 --- a/doc/sample_html/cogapp_test_cogapp_py.html +++ b/doc/sample_html/cogapp_test_cogapp_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/test_cogapp.py: 99.86% + Coverage for cogapp/test_cogapp.py: 30.00% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,4472 +51,2493 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

-

103

-

104

-

105

-

106

-

107

-

108

-

109

-

110

-

111

-

112

-

113

-

114

-

115

-

116

-

117

-

118

-

119

-

120

-

121

-

122

-

123

-

124

-

125

-

126

-

127

-

128

-

129

-

130

-

131

-

132

-

133

-

134

-

135

-

136

-

137

-

138

-

139

-

140

-

141

-

142

-

143

-

144

-

145

-

146

-

147

-

148

-

149

-

150

-

151

-

152

-

153

-

154

-

155

-

156

-

157

-

158

-

159

-

160

-

161

-

162

-

163

-

164

-

165

-

166

-

167

-

168

-

169

-

170

-

171

-

172

-

173

-

174

-

175

-

176

-

177

-

178

-

179

-

180

-

181

-

182

-

183

-

184

-

185

-

186

-

187

-

188

-

189

-

190

-

191

-

192

-

193

-

194

-

195

-

196

-

197

-

198

-

199

-

200

-

201

-

202

-

203

-

204

-

205

-

206

-

207

-

208

-

209

-

210

-

211

-

212

-

213

-

214

-

215

-

216

-

217

-

218

-

219

-

220

-

221

-

222

-

223

-

224

-

225

-

226

-

227

-

228

-

229

-

230

-

231

-

232

-

233

-

234

-

235

-

236

-

237

-

238

-

239

-

240

-

241

-

242

-

243

-

244

-

245

-

246

-

247

-

248

-

249

-

250

-

251

-

252

-

253

-

254

-

255

-

256

-

257

-

258

-

259

-

260

-

261

-

262

-

263

-

264

-

265

-

266

-

267

-

268

-

269

-

270

-

271

-

272

-

273

-

274

-

275

-

276

-

277

-

278

-

279

-

280

-

281

-

282

-

283

-

284

-

285

-

286

-

287

-

288

-

289

-

290

-

291

-

292

-

293

-

294

-

295

-

296

-

297

-

298

-

299

-

300

-

301

-

302

-

303

-

304

-

305

-

306

-

307

-

308

-

309

-

310

-

311

-

312

-

313

-

314

-

315

-

316

-

317

-

318

-

319

-

320

-

321

-

322

-

323

-

324

-

325

-

326

-

327

-

328

-

329

-

330

-

331

-

332

-

333

-

334

-

335

-

336

-

337

-

338

-

339

-

340

-

341

-

342

-

343

-

344

-

345

-

346

-

347

-

348

-

349

-

350

-

351

-

352

-

353

-

354

-

355

-

356

-

357

-

358

-

359

-

360

-

361

-

362

-

363

-

364

-

365

-

366

-

367

-

368

-

369

-

370

-

371

-

372

-

373

-

374

-

375

-

376

-

377

-

378

-

379

-

380

-

381

-

382

-

383

-

384

-

385

-

386

-

387

-

388

-

389

-

390

-

391

-

392

-

393

-

394

-

395

-

396

-

397

-

398

-

399

-

400

-

401

-

402

-

403

-

404

-

405

-

406

-

407

-

408

-

409

-

410

-

411

-

412

-

413

-

414

-

415

-

416

-

417

-

418

-

419

-

420

-

421

-

422

-

423

-

424

-

425

-

426

-

427

-

428

-

429

-

430

-

431

-

432

-

433

-

434

-

435

-

436

-

437

-

438

-

439

-

440

-

441

-

442

-

443

-

444

-

445

-

446

-

447

-

448

-

449

-

450

-

451

-

452

-

453

-

454

-

455

-

456

-

457

-

458

-

459

-

460

-

461

-

462

-

463

-

464

-

465

-

466

-

467

-

468

-

469

-

470

-

471

-

472

-

473

-

474

-

475

-

476

-

477

-

478

-

479

-

480

-

481

-

482

-

483

-

484

-

485

-

486

-

487

-

488

-

489

-

490

-

491

-

492

-

493

-

494

-

495

-

496

-

497

-

498

-

499

-

500

-

501

-

502

-

503

-

504

-

505

-

506

-

507

-

508

-

509

-

510

-

511

-

512

-

513

-

514

-

515

-

516

-

517

-

518

-

519

-

520

-

521

-

522

-

523

-

524

-

525

-

526

-

527

-

528

-

529

-

530

-

531

-

532

-

533

-

534

-

535

-

536

-

537

-

538

-

539

-

540

-

541

-

542

-

543

-

544

-

545

-

546

-

547

-

548

-

549

-

550

-

551

-

552

-

553

-

554

-

555

-

556

-

557

-

558

-

559

-

560

-

561

-

562

-

563

-

564

-

565

-

566

-

567

-

568

-

569

-

570

-

571

-

572

-

573

-

574

-

575

-

576

-

577

-

578

-

579

-

580

-

581

-

582

-

583

-

584

-

585

-

586

-

587

-

588

-

589

-

590

-

591

-

592

-

593

-

594

-

595

-

596

-

597

-

598

-

599

-

600

-

601

-

602

-

603

-

604

-

605

-

606

-

607

-

608

-

609

-

610

-

611

-

612

-

613

-

614

-

615

-

616

-

617

-

618

-

619

-

620

-

621

-

622

-

623

-

624

-

625

-

626

-

627

-

628

-

629

-

630

-

631

-

632

-

633

-

634

-

635

-

636

-

637

-

638

-

639

-

640

-

641

-

642

-

643

-

644

-

645

-

646

-

647

-

648

-

649

-

650

-

651

-

652

-

653

-

654

-

655

-

656

-

657

-

658

-

659

-

660

-

661

-

662

-

663

-

664

-

665

-

666

-

667

-

668

-

669

-

670

-

671

-

672

-

673

-

674

-

675

-

676

-

677

-

678

-

679

-

680

-

681

-

682

-

683

-

684

-

685

-

686

-

687

-

688

-

689

-

690

-

691

-

692

-

693

-

694

-

695

-

696

-

697

-

698

-

699

-

700

-

701

-

702

-

703

-

704

-

705

-

706

-

707

-

708

-

709

-

710

-

711

-

712

-

713

-

714

-

715

-

716

-

717

-

718

-

719

-

720

-

721

-

722

-

723

-

724

-

725

-

726

-

727

-

728

-

729

-

730

-

731

-

732

-

733

-

734

-

735

-

736

-

737

-

738

-

739

-

740

-

741

-

742

-

743

-

744

-

745

-

746

-

747

-

748

-

749

-

750

-

751

-

752

-

753

-

754

-

755

-

756

-

757

-

758

-

759

-

760

-

761

-

762

-

763

-

764

-

765

-

766

-

767

-

768

-

769

-

770

-

771

-

772

-

773

-

774

-

775

-

776

-

777

-

778

-

779

-

780

-

781

-

782

-

783

-

784

-

785

-

786

-

787

-

788

-

789

-

790

-

791

-

792

-

793

-

794

-

795

-

796

-

797

-

798

-

799

-

800

-

801

-

802

-

803

-

804

-

805

-

806

-

807

-

808

-

809

-

810

-

811

-

812

-

813

-

814

-

815

-

816

-

817

-

818

-

819

-

820

-

821

-

822

-

823

-

824

-

825

-

826

-

827

-

828

-

829

-

830

-

831

-

832

-

833

-

834

-

835

-

836

-

837

-

838

-

839

-

840

-

841

-

842

-

843

-

844

-

845

-

846

-

847

-

848

-

849

-

850

-

851

-

852

-

853

-

854

-

855

-

856

-

857

-

858

-

859

-

860

-

861

-

862

-

863

-

864

-

865

-

866

-

867

-

868

-

869

-

870

-

871

-

872

-

873

-

874

-

875

-

876

-

877

-

878

-

879

-

880

-

881

-

882

-

883

-

884

-

885

-

886

-

887

-

888

-

889

-

890

-

891

-

892

-

893

-

894

-

895

-

896

-

897

-

898

-

899

-

900

-

901

-

902

-

903

-

904

-

905

-

906

-

907

-

908

-

909

-

910

-

911

-

912

-

913

-

914

-

915

-

916

-

917

-

918

-

919

-

920

-

921

-

922

-

923

-

924

-

925

-

926

-

927

-

928

-

929

-

930

-

931

-

932

-

933

-

934

-

935

-

936

-

937

-

938

-

939

-

940

-

941

-

942

-

943

-

944

-

945

-

946

-

947

-

948

-

949

-

950

-

951

-

952

-

953

-

954

-

955

-

956

-

957

-

958

-

959

-

960

-

961

-

962

-

963

-

964

-

965

-

966

-

967

-

968

-

969

-

970

-

971

-

972

-

973

-

974

-

975

-

976

-

977

-

978

-

979

-

980

-

981

-

982

-

983

-

984

-

985

-

986

-

987

-

988

-

989

-

990

-

991

-

992

-

993

-

994

-

995

-

996

-

997

-

998

-

999

-

1000

-

1001

-

1002

-

1003

-

1004

-

1005

-

1006

-

1007

-

1008

-

1009

-

1010

-

1011

-

1012

-

1013

-

1014

-

1015

-

1016

-

1017

-

1018

-

1019

-

1020

-

1021

-

1022

-

1023

-

1024

-

1025

-

1026

-

1027

-

1028

-

1029

-

1030

-

1031

-

1032

-

1033

-

1034

-

1035

-

1036

-

1037

-

1038

-

1039

-

1040

-

1041

-

1042

-

1043

-

1044

-

1045

-

1046

-

1047

-

1048

-

1049

-

1050

-

1051

-

1052

-

1053

-

1054

-

1055

-

1056

-

1057

-

1058

-

1059

-

1060

-

1061

-

1062

-

1063

-

1064

-

1065

-

1066

-

1067

-

1068

-

1069

-

1070

-

1071

-

1072

-

1073

-

1074

-

1075

-

1076

-

1077

-

1078

-

1079

-

1080

-

1081

-

1082

-

1083

-

1084

-

1085

-

1086

-

1087

-

1088

-

1089

-

1090

-

1091

-

1092

-

1093

-

1094

-

1095

-

1096

-

1097

-

1098

-

1099

-

1100

-

1101

-

1102

-

1103

-

1104

-

1105

-

1106

-

1107

-

1108

-

1109

-

1110

-

1111

-

1112

-

1113

-

1114

-

1115

-

1116

-

1117

-

1118

-

1119

-

1120

-

1121

-

1122

-

1123

-

1124

-

1125

-

1126

-

1127

-

1128

-

1129

-

1130

-

1131

-

1132

-

1133

-

1134

-

1135

-

1136

-

1137

-

1138

-

1139

-

1140

-

1141

-

1142

-

1143

-

1144

-

1145

-

1146

-

1147

-

1148

-

1149

-

1150

-

1151

-

1152

-

1153

-

1154

-

1155

-

1156

-

1157

-

1158

-

1159

-

1160

-

1161

-

1162

-

1163

-

1164

-

1165

-

1166

-

1167

-

1168

-

1169

-

1170

-

1171

-

1172

-

1173

-

1174

-

1175

-

1176

-

1177

-

1178

-

1179

-

1180

-

1181

-

1182

-

1183

-

1184

-

1185

-

1186

-

1187

-

1188

-

1189

-

1190

-

1191

-

1192

-

1193

-

1194

-

1195

-

1196

-

1197

-

1198

-

1199

-

1200

-

1201

-

1202

-

1203

-

1204

-

1205

-

1206

-

1207

-

1208

-

1209

-

1210

-

1211

-

1212

-

1213

-

1214

-

1215

-

1216

-

1217

-

1218

-

1219

-

1220

-

1221

-

1222

-

1223

-

1224

-

1225

-

1226

-

1227

-

1228

-

1229

-

1230

-

1231

-

1232

-

1233

-

1234

-

1235

-

1236

-

1237

-

1238

-

1239

-

1240

-

1241

-

1242

-

1243

-

1244

-

1245

-

1246

-

1247

-

1248

-

1249

-

1250

-

1251

-

1252

-

1253

-

1254

-

1255

-

1256

-

1257

-

1258

-

1259

-

1260

-

1261

-

1262

-

1263

-

1264

-

1265

-

1266

-

1267

-

1268

-

1269

-

1270

-

1271

-

1272

-

1273

-

1274

-

1275

-

1276

-

1277

-

1278

-

1279

-

1280

-

1281

-

1282

-

1283

-

1284

-

1285

-

1286

-

1287

-

1288

-

1289

-

1290

-

1291

-

1292

-

1293

-

1294

-

1295

-

1296

-

1297

-

1298

-

1299

-

1300

-

1301

-

1302

-

1303

-

1304

-

1305

-

1306

-

1307

-

1308

-

1309

-

1310

-

1311

-

1312

-

1313

-

1314

-

1315

-

1316

-

1317

-

1318

-

1319

-

1320

-

1321

-

1322

-

1323

-

1324

-

1325

-

1326

-

1327

-

1328

-

1329

-

1330

-

1331

-

1332

-

1333

-

1334

-

1335

-

1336

-

1337

-

1338

-

1339

-

1340

-

1341

-

1342

-

1343

-

1344

-

1345

-

1346

-

1347

-

1348

-

1349

-

1350

-

1351

-

1352

-

1353

-

1354

-

1355

-

1356

-

1357

-

1358

-

1359

-

1360

-

1361

-

1362

-

1363

-

1364

-

1365

-

1366

-

1367

-

1368

-

1369

-

1370

-

1371

-

1372

-

1373

-

1374

-

1375

-

1376

-

1377

-

1378

-

1379

-

1380

-

1381

-

1382

-

1383

-

1384

-

1385

-

1386

-

1387

-

1388

-

1389

-

1390

-

1391

-

1392

-

1393

-

1394

-

1395

-

1396

-

1397

-

1398

-

1399

-

1400

-

1401

-

1402

-

1403

-

1404

-

1405

-

1406

-

1407

-

1408

-

1409

-

1410

-

1411

-

1412

-

1413

-

1414

-

1415

-

1416

-

1417

-

1418

-

1419

-

1420

-

1421

-

1422

-

1423

-

1424

-

1425

-

1426

-

1427

-

1428

-

1429

-

1430

-

1431

-

1432

-

1433

-

1434

-

1435

-

1436

-

1437

-

1438

-

1439

-

1440

-

1441

-

1442

-

1443

-

1444

-

1445

-

1446

-

1447

-

1448

-

1449

-

1450

-

1451

-

1452

-

1453

-

1454

-

1455

-

1456

-

1457

-

1458

-

1459

-

1460

-

1461

-

1462

-

1463

-

1464

-

1465

-

1466

-

1467

-

1468

-

1469

-

1470

-

1471

-

1472

-

1473

-

1474

-

1475

-

1476

-

1477

-

1478

-

1479

-

1480

-

1481

-

1482

-

1483

-

1484

-

1485

-

1486

-

1487

-

1488

-

1489

-

1490

-

1491

-

1492

-

1493

-

1494

-

1495

-

1496

-

1497

-

1498

-

1499

-

1500

-

1501

-

1502

-

1503

-

1504

-

1505

-

1506

-

1507

-

1508

-

1509

-

1510

-

1511

-

1512

-

1513

-

1514

-

1515

-

1516

-

1517

-

1518

-

1519

-

1520

-

1521

-

1522

-

1523

-

1524

-

1525

-

1526

-

1527

-

1528

-

1529

-

1530

-

1531

-

1532

-

1533

-

1534

-

1535

-

1536

-

1537

-

1538

-

1539

-

1540

-

1541

-

1542

-

1543

-

1544

-

1545

-

1546

-

1547

-

1548

-

1549

-

1550

-

1551

-

1552

-

1553

-

1554

-

1555

-

1556

-

1557

-

1558

-

1559

-

1560

-

1561

-

1562

-

1563

-

1564

-

1565

-

1566

-

1567

-

1568

-

1569

-

1570

-

1571

-

1572

-

1573

-

1574

-

1575

-

1576

-

1577

-

1578

-

1579

-

1580

-

1581

-

1582

-

1583

-

1584

-

1585

-

1586

-

1587

-

1588

-

1589

-

1590

-

1591

-

1592

-

1593

-

1594

-

1595

-

1596

-

1597

-

1598

-

1599

-

1600

-

1601

-

1602

-

1603

-

1604

-

1605

-

1606

-

1607

-

1608

-

1609

-

1610

-

1611

-

1612

-

1613

-

1614

-

1615

-

1616

-

1617

-

1618

-

1619

-

1620

-

1621

-

1622

-

1623

-

1624

-

1625

-

1626

-

1627

-

1628

-

1629

-

1630

-

1631

-

1632

-

1633

-

1634

-

1635

-

1636

-

1637

-

1638

-

1639

-

1640

-

1641

-

1642

-

1643

-

1644

-

1645

-

1646

-

1647

-

1648

-

1649

-

1650

-

1651

-

1652

-

1653

-

1654

-

1655

-

1656

-

1657

-

1658

-

1659

-

1660

-

1661

-

1662

-

1663

-

1664

-

1665

-

1666

-

1667

-

1668

-

1669

-

1670

-

1671

-

1672

-

1673

-

1674

-

1675

-

1676

-

1677

-

1678

-

1679

-

1680

-

1681

-

1682

-

1683

-

1684

-

1685

-

1686

-

1687

-

1688

-

1689

-

1690

-

1691

-

1692

-

1693

-

1694

-

1695

-

1696

-

1697

-

1698

-

1699

-

1700

-

1701

-

1702

-

1703

-

1704

-

1705

-

1706

-

1707

-

1708

-

1709

-

1710

-

1711

-

1712

-

1713

-

1714

-

1715

-

1716

-

1717

-

1718

-

1719

-

1720

-

1721

-

1722

-

1723

-

1724

-

1725

-

1726

-

1727

-

1728

-

1729

-

1730

-

1731

-

1732

-

1733

-

1734

-

1735

-

1736

-

1737

-

1738

-

1739

-

1740

-

1741

-

1742

-

1743

-

1744

-

1745

-

1746

-

1747

-

1748

-

1749

-

1750

-

1751

-

1752

-

1753

-

1754

-

1755

-

1756

-

1757

-

1758

-

1759

-

1760

-

1761

-

1762

-

1763

-

1764

-

1765

-

1766

-

1767

-

1768

-

1769

-

1770

-

1771

-

1772

-

1773

-

1774

-

1775

-

1776

-

1777

-

1778

-

1779

-

1780

-

1781

-

1782

-

1783

-

1784

-

1785

-

1786

-

1787

-

1788

-

1789

-

1790

-

1791

-

1792

-

1793

-

1794

-

1795

-

1796

-

1797

-

1798

-

1799

-

1800

-

1801

-

1802

-

1803

-

1804

-

1805

-

1806

-

1807

-

1808

-

1809

-

1810

-

1811

-

1812

-

1813

-

1814

-

1815

-

1816

-

1817

-

1818

-

1819

-

1820

-

1821

-

1822

-

1823

-

1824

-

1825

-

1826

-

1827

-

1828

-

1829

-

1830

-

1831

-

1832

-

1833

-

1834

-

1835

-

1836

-

1837

-

1838

-

1839

-

1840

-

1841

-

1842

-

1843

-

1844

-

1845

-

1846

-

1847

-

1848

-

1849

-

1850

-

1851

-

1852

-

1853

-

1854

-

1855

-

1856

-

1857

-

1858

-

1859

-

1860

-

1861

-

1862

-

1863

-

1864

-

1865

-

1866

-

1867

-

1868

-

1869

-

1870

-

1871

-

1872

-

1873

-

1874

-

1875

-

1876

-

1877

-

1878

-

1879

-

1880

-

1881

-

1882

-

1883

-

1884

-

1885

-

1886

-

1887

-

1888

-

1889

-

1890

-

1891

-

1892

-

1893

-

1894

-

1895

-

1896

-

1897

-

1898

-

1899

-

1900

-

1901

-

1902

-

1903

-

1904

-

1905

-

1906

-

1907

-

1908

-

1909

-

1910

-

1911

-

1912

-

1913

-

1914

-

1915

-

1916

-

1917

-

1918

-

1919

-

1920

-

1921

-

1922

-

1923

-

1924

-

1925

-

1926

-

1927

-

1928

-

1929

-

1930

-

1931

-

1932

-

1933

-

1934

-

1935

-

1936

-

1937

-

1938

-

1939

-

1940

-

1941

-

1942

-

1943

-

1944

-

1945

-

1946

-

1947

-

1948

-

1949

-

1950

-

1951

-

1952

-

1953

-

1954

-

1955

-

1956

-

1957

-

1958

-

1959

-

1960

-

1961

-

1962

-

1963

-

1964

-

1965

-

1966

-

1967

-

1968

-

1969

-

1970

-

1971

-

1972

-

1973

-

1974

-

1975

-

1976

-

1977

-

1978

-

1979

-

1980

-

1981

-

1982

-

1983

-

1984

-

1985

-

1986

-

1987

-

1988

-

1989

-

1990

-

1991

-

1992

-

1993

-

1994

-

1995

-

1996

-

1997

-

1998

-

1999

-

2000

-

2001

-

2002

-

2003

-

2004

-

2005

-

2006

-

2007

-

2008

-

2009

-

2010

-

2011

-

2012

-

2013

-

2014

-

2015

-

2016

-

2017

-

2018

-

2019

-

2020

-

2021

-

2022

-

2023

-

2024

-

2025

-

2026

-

2027

-

2028

-

2029

-

2030

-

2031

-

2032

-

2033

-

2034

-

2035

-

2036

-

2037

-

2038

-

2039

-

2040

-

2041

-

2042

-

2043

-

2044

-

2045

-

2046

-

2047

-

2048

-

2049

-

2050

-

2051

-

2052

-

2053

-

2054

-

2055

-

2056

-

2057

-

2058

-

2059

-

2060

-

2061

-

2062

-

2063

-

2064

-

2065

-

2066

-

2067

-

2068

-

2069

-

2070

-

2071

-

2072

-

2073

-

2074

-

2075

-

2076

-

2077

-

2078

-

2079

-

2080

-

2081

-

2082

-

2083

-

2084

-

2085

-

2086

-

2087

-

2088

-

2089

-

2090

-

2091

-

2092

-

2093

-

2094

-

2095

-

2096

-

2097

-

2098

-

2099

-

2100

-

2101

-

2102

-

2103

-

2104

-

2105

-

2106

-

2107

-

2108

-

2109

-

2110

-

2111

-

2112

-

2113

-

2114

-

2115

-

2116

-

2117

-

2118

-

2119

-

2120

-

2121

-

2122

-

2123

-

2124

-

2125

-

2126

-

2127

-

2128

-

2129

-

2130

-

2131

-

2132

-

2133

-

2134

-

2135

-

2136

-

2137

-

2138

-

2139

-

2140

-

2141

-

2142

-

2143

-

2144

-

2145

-

2146

-

2147

-

2148

-

2149

-

2150

-

2151

-

2152

-

2153

-

2154

-

2155

-

2156

-

2157

-

2158

-

2159

-

2160

-

2161

-

2162

-

2163

-

2164

-

2165

-

2166

-

2167

-

2168

-

2169

-

2170

-

2171

-

2172

-

2173

-

2174

-

2175

-

2176

-

2177

-

2178

-

2179

-

2180

-

2181

-

2182

-

2183

-

2184

-

2185

-

2186

-

2187

-

2188

-

2189

-

2190

-

2191

-

2192

-

2193

-

2194

-

2195

-

2196

-

2197

-

2198

-

2199

-

2200

-

2201

-

2202

-

2203

-

2204

-

2205

-

2206

-

2207

-

2208

-

2209

-

2210

-

2211

-

2212

-

2213

-

2214

-

2215

-

2216

-

2217

-

2218

-

2219

-

2220

-

2221

-

2222

- -
-

""" Test cogapp. 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2015, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import 

-

 

-

import os, os.path, random, re, shutil, stat, sys, tempfile 

-

 

-

# Use unittest2 if it's available, otherwise unittest. This gives us 

-

# back-ported features for 2.6. 

-

try: 

-

import unittest2 as unittest 

-

except ImportError: 

-

import unittest 

-

 

-

from .backward import StringIO, to_bytes, b 

-

from .cogapp import Cog, CogOptions, CogGenerator 

-

from .cogapp import CogError, CogUsageError, CogGeneratedError 

-

from .cogapp import usage, __version__ 

-

from .whiteutils import reindentBlock 

-

from .makefiles import * 

-

 

-

 

-

TestCase = unittest.TestCase 

-

 

-

 

-

class CogTestsInMemory(TestCase): 

-

""" Test cases for cogapp.Cog() 

-

""" 

-

 

-

def testNoCog(self): 

-

strings = [ 

-

'', 

-

' ', 

-

' \t \t \tx', 

-

'hello', 

-

'the cat\nin the\nhat.', 

-

'Horton\n\tHears A\n\t\tWho' 

-

] 

-

for s in strings: 

-

self.assertEqual(Cog().processString(s), s) 

-

 

-

def testSimple(self): 

-

infile = """\ 

-

Some text. 

-

//[[[cog 

-

import cog 

-

cog.outl("This is line one\\n") 

-

cog.outl("This is line two") 

-

//]]] 

-

gobbledegook. 

-

//[[[end]]] 

-

epilogue. 

-

""" 

-

 

-

outfile = """\ 

-

Some text. 

-

//[[[cog 

-

import cog 

-

cog.outl("This is line one\\n") 

-

cog.outl("This is line two") 

-

//]]] 

-

This is line one 

-

 

-

This is line two 

-

//[[[end]]] 

-

epilogue. 

-

""" 

-

 

-

self.assertEqual(Cog().processString(infile), outfile) 

-

 

-

def testEmptyCog(self): 

-

# The cog clause can be totally empty. Not sure why you'd want it, 

-

# but it works. 

-

infile = """\ 

-

hello 

-

//[[[cog 

-

//]]] 

-

//[[[end]]] 

-

goodbye 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testMultipleCogs(self): 

-

# One file can have many cog chunks, even abutting each other. 

-

infile = """\ 

-

//[[[cog 

-

cog.out("chunk1") 

-

//]]] 

-

chunk1 

-

//[[[end]]] 

-

//[[[cog 

-

cog.out("chunk2") 

-

//]]] 

-

chunk2 

-

//[[[end]]] 

-

between chunks 

-

//[[[cog 

-

cog.out("chunk3") 

-

//]]] 

-

chunk3 

-

//[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testTrimBlankLines(self): 

-

infile = """\ 

-

//[[[cog 

-

cog.out("This is line one\\n", trimblanklines=True) 

-

cog.out(''' 

-

This is line two 

-

''', dedent=True, trimblanklines=True) 

-

cog.outl("This is line three", trimblanklines=True) 

-

//]]] 

-

This is line one 

-

This is line two 

-

This is line three 

-

//[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testTrimEmptyBlankLines(self): 

-

infile = """\ 

-

//[[[cog 

-

cog.out("This is line one\\n", trimblanklines=True) 

-

cog.out(''' 

-

This is line two 

-

''', dedent=True, trimblanklines=True) 

-

cog.out('', dedent=True, trimblanklines=True) 

-

cog.outl("This is line three", trimblanklines=True) 

-

//]]] 

-

This is line one 

-

This is line two 

-

This is line three 

-

//[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def test22EndOfLine(self): 

-

# In Python 2.2, this cog file was not parsing because the 

-

# last line is indented but didn't end with a newline. 

-

infile = """\ 

-

//[[[cog 

-

import cog 

-

for i in range(3): 

-

cog.out("%d\\n" % i) 

-

//]]] 

-

0 

-

1 

-

2 

-

//[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testIndentedCode(self): 

-

infile = """\ 

-

first line 

-

[[[cog 

-

import cog 

-

for i in range(3): 

-

cog.out("xx%d\\n" % i) 

-

]]] 

-

xx0 

-

xx1 

-

xx2 

-

[[[end]]] 

-

last line 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testPrefixedCode(self): 

-

infile = """\ 

-

--[[[cog 

-

--import cog 

-

--for i in range(3): 

-

-- cog.out("xx%d\\n" % i) 

-

--]]] 

-

xx0 

-

xx1 

-

xx2 

-

--[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testPrefixedIndentedCode(self): 

-

infile = """\ 

-

prologue 

-

--[[[cog 

-

-- import cog 

-

-- for i in range(3): 

-

-- cog.out("xy%d\\n" % i) 

-

--]]] 

-

xy0 

-

xy1 

-

xy2 

-

--[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testBogusPrefixMatch(self): 

-

infile = """\ 

-

prologue 

-

#[[[cog 

-

import cog 

-

# This comment should not be clobbered by removing the pound sign. 

-

for i in range(3): 

-

cog.out("xy%d\\n" % i) 

-

#]]] 

-

xy0 

-

xy1 

-

xy2 

-

#[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testNoFinalNewline(self): 

-

# If the cog'ed output has no final newline, 

-

# it shouldn't eat up the cog terminator. 

-

infile = """\ 

-

prologue 

-

[[[cog 

-

import cog 

-

for i in range(3): 

-

cog.out("%d" % i) 

-

]]] 

-

012 

-

[[[end]]] 

-

epilogue 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testNoOutputAtAll(self): 

-

# If there is absolutely no cog output, that's ok. 

-

infile = """\ 

-

prologue 

-

[[[cog 

-

i = 1 

-

]]] 

-

[[[end]]] 

-

epilogue 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testPurelyBlankLine(self): 

-

# If there is a blank line in the cog code with no whitespace 

-

# prefix, that should be OK. 

-

 

-

infile = """\ 

-

prologue 

-

[[[cog 

-

import sys 

-

cog.out("Hello") 

-

$ 

-

cog.out("There") 

-

]]] 

-

HelloThere 

-

[[[end]]] 

-

epilogue 

-

""" 

-

 

-

infile = reindentBlock(infile.replace('$', '')) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testEmptyOutl(self): 

-

# Alexander Belchenko suggested the string argument to outl should 

-

# be optional. Does it work? 

-

 

-

infile = """\ 

-

prologue 

-

[[[cog 

-

cog.outl("x") 

-

cog.outl() 

-

cog.outl("y") 

-

cog.outl(trimblanklines=True) 

-

cog.outl("z") 

-

]]] 

-

x 

-

 

-

y 

-

 

-

z 

-

[[[end]]] 

-

epilogue 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testFirstLineNum(self): 

-

infile = """\ 

-

fooey 

-

[[[cog 

-

cog.outl("started at line number %d" % cog.firstLineNum) 

-

]]] 

-

started at line number 2 

-

[[[end]]] 

-

blah blah 

-

[[[cog 

-

cog.outl("and again at line %d" % cog.firstLineNum) 

-

]]] 

-

and again at line 8 

-

[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

def testCompactOneLineCode(self): 

-

infile = """\ 

-

first line 

-

hey: [[[cog cog.outl("hello %d" % (3*3*3*3)) ]]] looky! 

-

get rid of this! 

-

[[[end]]] 

-

last line 

-

""" 

-

 

-

outfile = """\ 

-

first line 

-

hey: [[[cog cog.outl("hello %d" % (3*3*3*3)) ]]] looky! 

-

hello 81 

-

[[[end]]] 

-

last line 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) 

-

 

-

def testInsideOutCompact(self): 

-

infile = """\ 

-

first line 

-

hey?: ]]] what is this? [[[cog strange! 

-

get rid of this! 

-

[[[end]]] 

-

last line 

-

""" 

-

with self.assertRaisesRegexp(CogError, r"infile.txt\(2\): Cog code markers inverted"): 

-

Cog().processString(reindentBlock(infile), "infile.txt") 

-

 

-

def testSharingGlobals(self): 

-

infile = """\ 

-

first line 

-

hey: [[[cog s="hey there" ]]] looky! 

-

[[[end]]] 

-

more literal junk. 

-

[[[cog cog.outl(s) ]]] 

-

[[[end]]] 

-

last line 

-

""" 

-

 

-

outfile = """\ 

-

first line 

-

hey: [[[cog s="hey there" ]]] looky! 

-

[[[end]]] 

-

more literal junk. 

-

[[[cog cog.outl(s) ]]] 

-

hey there 

-

[[[end]]] 

-

last line 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) 

-

 

-

def testAssertInCogCode(self): 

-

# Check that we can test assertions in cog code in the test framework. 

-

infile = """\ 

-

[[[cog 

-

assert 1 == 2, "Oops" 

-

]]] 

-

[[[end]]] 

-

""" 

-

infile = reindentBlock(infile) 

-

with self.assertRaisesRegexp(AssertionError, "Oops"): 

-

Cog().processString(infile) 

-

 

-

def testCogPrevious(self): 

-

# Check that we can access the previous run's output. 

-

infile = """\ 

-

[[[cog 

-

assert cog.previous == "Hello there!\\n", "WTF??" 

-

cog.out(cog.previous) 

-

cog.outl("Ran again!") 

-

]]] 

-

Hello there! 

-

[[[end]]] 

-

""" 

-

 

-

outfile = """\ 

-

[[[cog 

-

assert cog.previous == "Hello there!\\n", "WTF??" 

-

cog.out(cog.previous) 

-

cog.outl("Ran again!") 

-

]]] 

-

Hello there! 

-

Ran again! 

-

[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) 

-

 

-

 

-

class CogOptionsTests(TestCase): 

-

""" Test the CogOptions class. 

-

""" 

-

 

-

def testEquality(self): 

-

o = CogOptions() 

-

p = CogOptions() 

-

self.assertEqual(o, p) 

-

o.parseArgs(['-r']) 

-

self.assertNotEqual(o, p) 

-

p.parseArgs(['-r']) 

-

self.assertEqual(o, p) 

-

 

-

def testCloning(self): 

-

o = CogOptions() 

-

o.parseArgs(['-I', 'fooey', '-I', 'booey', '-s', ' /*x*/']) 

-

p = o.clone() 

-

self.assertEqual(o, p) 

-

p.parseArgs(['-I', 'huey', '-D', 'foo=quux']) 

-

self.assertNotEqual(o, p) 

-

q = CogOptions() 

-

q.parseArgs(['-I', 'fooey', '-I', 'booey', '-s', ' /*x*/', '-I', 'huey', '-D', 'foo=quux']) 

-

self.assertEqual(p, q) 

-

 

-

def testCombiningFlags(self): 

-

# Single-character flags can be combined. 

-

o = CogOptions() 

-

o.parseArgs(['-e', '-r', '-z']) 

-

p = CogOptions() 

-

p.parseArgs(['-erz']) 

-

self.assertEqual(o, p) 

-

 

-

def testMarkers(self): 

-

o = CogOptions() 

-

o._parse_markers('a b c') 

-

self.assertEqual('a', o.sBeginSpec) 

-

self.assertEqual('b', o.sEndSpec) 

-

self.assertEqual('c', o.sEndOutput) 

-

 

-

def testMarkersSwitch(self): 

-

o = CogOptions() 

-

o.parseArgs(['--markers', 'a b c']) 

-

self.assertEqual('a', o.sBeginSpec) 

-

self.assertEqual('b', o.sEndSpec) 

-

self.assertEqual('c', o.sEndOutput) 

-

 

-

 

-

class FileStructureTests(TestCase): 

-

""" Test cases to check that we're properly strict about the structure 

-

of files. 

-

""" 

-

 

-

def isBad(self, infile, msg=None): 

-

infile = reindentBlock(infile) 

-

with self.assertRaisesRegexp(CogError, re.escape(msg)): 

-

Cog().processString(infile, 'infile.txt') 

-

 

-

def testBeginNoEnd(self): 

-

infile = """\ 

-

Fooey 

-

#[[[cog 

-

cog.outl('hello') 

-

""" 

-

self.isBad(infile, "infile.txt(2): Cog block begun but never ended.") 

-

 

-

def testNoEoo(self): 

-

infile = """\ 

-

Fooey 

-

#[[[cog 

-

cog.outl('hello') 

-

#]]] 

-

""" 

-

self.isBad(infile, "infile.txt(4): Missing '[[[end]]]' before end of file.") 

-

 

-

infile2 = """\ 

-

Fooey 

-

#[[[cog 

-

cog.outl('hello') 

-

#]]] 

-

#[[[cog 

-

cog.outl('goodbye') 

-

#]]] 

-

""" 

-

self.isBad(infile2, "infile.txt(5): Unexpected '[[[cog'") 

-

 

-

def testStartWithEnd(self): 

-

infile = """\ 

-

#]]] 

-

""" 

-

self.isBad(infile, "infile.txt(1): Unexpected ']]]'") 

-

 

-

infile2 = """\ 

-

#[[[cog 

-

cog.outl('hello') 

-

#]]] 

-

#[[[end]]] 

-

#]]] 

-

""" 

-

self.isBad(infile2, "infile.txt(5): Unexpected ']]]'") 

-

 

-

def testStartWithEoo(self): 

-

infile = """\ 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile, "infile.txt(1): Unexpected '[[[end]]]'") 

-

 

-

infile2 = """\ 

-

#[[[cog 

-

cog.outl('hello') 

-

#]]] 

-

#[[[end]]] 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile2, "infile.txt(5): Unexpected '[[[end]]]'") 

-

 

-

def testNoEnd(self): 

-

infile = """\ 

-

#[[[cog 

-

cog.outl("hello") 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile, "infile.txt(3): Unexpected '[[[end]]]'") 

-

 

-

infile2 = """\ 

-

#[[[cog 

-

cog.outl('hello') 

-

#]]] 

-

#[[[end]]] 

-

#[[[cog 

-

cog.outl("hello") 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile2, "infile.txt(7): Unexpected '[[[end]]]'") 

-

 

-

def testTwoBegins(self): 

-

infile = """\ 

-

#[[[cog 

-

#[[[cog 

-

cog.outl("hello") 

-

#]]] 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile, "infile.txt(2): Unexpected '[[[cog'") 

-

 

-

infile2 = """\ 

-

#[[[cog 

-

cog.outl("hello") 

-

#]]] 

-

#[[[end]]] 

-

#[[[cog 

-

#[[[cog 

-

cog.outl("hello") 

-

#]]] 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile2, "infile.txt(6): Unexpected '[[[cog'") 

-

 

-

def testTwoEnds(self): 

-

infile = """\ 

-

#[[[cog 

-

cog.outl("hello") 

-

#]]] 

-

#]]] 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile, "infile.txt(4): Unexpected ']]]'") 

-

 

-

infile2 = """\ 

-

#[[[cog 

-

cog.outl("hello") 

-

#]]] 

-

#[[[end]]] 

-

#[[[cog 

-

cog.outl("hello") 

-

#]]] 

-

#]]] 

-

#[[[end]]] 

-

""" 

-

self.isBad(infile2, "infile.txt(8): Unexpected ']]]'") 

-

 

-

 

-

class CogErrorTests(TestCase): 

-

""" Test cases for cog.error(). 

-

""" 

-

 

-

def testErrorMsg(self): 

-

infile = """\ 

-

[[[cog cog.error("This ain't right!")]]] 

-

[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

with self.assertRaisesRegexp(CogGeneratedError, "This ain't right!"): 

-

Cog().processString(infile) 

-

 

-

def testErrorNoMsg(self): 

-

infile = """\ 

-

[[[cog cog.error()]]] 

-

[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

with self.assertRaisesRegexp(CogGeneratedError, "Error raised by cog generator."): 

-

Cog().processString(infile) 

-

 

-

def testNoErrorIfErrorNotCalled(self): 

-

infile = """\ 

-

--[[[cog 

-

--import cog 

-

--for i in range(3): 

-

-- if i > 10: 

-

-- cog.error("Something is amiss!") 

-

-- cog.out("xx%d\\n" % i) 

-

--]]] 

-

xx0 

-

xx1 

-

xx2 

-

--[[[end]]] 

-

""" 

-

 

-

infile = reindentBlock(infile) 

-

self.assertEqual(Cog().processString(infile), infile) 

-

 

-

 

-

class CogGeneratorGetCodeTests(TestCase): 

-

""" Unit tests against CogGenerator to see if its getCode() method works 

-

properly. 

-

""" 

-

 

-

def setUp(self): 

-

""" All tests get a generator to use, and short same-length names for 

-

the functions we're going to use. 

-

""" 

-

self.gen = CogGenerator() 

-

self.m = self.gen.parseMarker 

-

self.l = self.gen.parseLine 

-

 

-

def testEmpty(self): 

-

self.m('// [[[cog') 

-

self.m('// ]]]') 

-

self.assertEqual(self.gen.getCode(), '') 

-

 

-

def testSimple(self): 

-

self.m('// [[[cog') 

-

self.l(' print "hello"') 

-

self.l(' print "bye"') 

-

self.m('// ]]]') 

-

self.assertEqual(self.gen.getCode(), 'print "hello"\nprint "bye"') 

-

 

-

def testCompressed1(self): 

-

# For a while, I supported compressed code blocks, but no longer. 

-

self.m('// [[[cog: print """') 

-

self.l('// hello') 

-

self.l('// bye') 

-

self.m('// """)]]]') 

-

self.assertEqual(self.gen.getCode(), 'hello\nbye') 

-

 

-

def testCompressed2(self): 

-

# For a while, I supported compressed code blocks, but no longer. 

-

self.m('// [[[cog: print """') 

-

self.l('hello') 

-

self.l('bye') 

-

self.m('// """)]]]') 

-

self.assertEqual(self.gen.getCode(), 'hello\nbye') 

-

 

-

def testCompressed3(self): 

-

# For a while, I supported compressed code blocks, but no longer. 

-

self.m('// [[[cog') 

-

self.l('print """hello') 

-

self.l('bye') 

-

self.m('// """)]]]') 

-

self.assertEqual(self.gen.getCode(), 'print """hello\nbye') 

-

 

-

def testCompressed4(self): 

-

# For a while, I supported compressed code blocks, but no longer. 

-

self.m('// [[[cog: print """') 

-

self.l('hello') 

-

self.l('bye""")') 

-

self.m('// ]]]') 

-

self.assertEqual(self.gen.getCode(), 'hello\nbye""")') 

-

 

-

def testNoCommonPrefixForMarkers(self): 

-

# It's important to be able to use #if 0 to hide lines from a 

-

# C++ compiler. 

-

self.m('#if 0 //[[[cog') 

-

self.l('\timport cog, sys') 

-

self.l('') 

-

self.l('\tprint sys.argv') 

-

self.m('#endif //]]]') 

-

self.assertEqual(self.gen.getCode(), 'import cog, sys\n\nprint sys.argv') 

-

 

-

 

-

class TestCaseWithTempDir(TestCase): 

-

 

-

def newCog(self): 

-

""" Initialize the cog members for another run. 

-

""" 

-

# Create a cog engine, and catch its output. 

-

self.cog = Cog() 

-

self.output = StringIO() 

-

self.cog.setOutput(stdout=self.output, stderr=self.output) 

-

 

-

def setUp(self): 

-

# Create a temporary directory. 

-

self.tempdir = os.path.join(tempfile.gettempdir(), 'testcog_tempdir_' + str(random.random())[2:]) 

-

os.mkdir(self.tempdir) 

-

self.olddir = os.getcwd() 

-

os.chdir(self.tempdir) 

-

self.newCog() 

-

 

-

def tearDown(self): 

-

os.chdir(self.olddir) 

-

# Get rid of the temporary directory. 

-

shutil.rmtree(self.tempdir) 

-

 

-

def assertFilesSame(self, sFName1, sFName2): 

-

text1 = open(os.path.join(self.tempdir, sFName1), 'rb').read() 

-

text2 = open(os.path.join(self.tempdir, sFName2), 'rb').read() 

-

self.assertEqual(text1, text2) 

-

 

-

def assertFileContent(self, sFName, sContent): 

-

sAbsName = os.path.join(self.tempdir, sFName) 

-

f = open(sAbsName, 'rb') 

-

try: 

-

sFileContent = f.read() 

-

finally: 

-

f.close() 

-

self.assertEqual(sFileContent, to_bytes(sContent)) 

-

 

-

 

-

class ArgumentHandlingTests(TestCaseWithTempDir): 

-

 

-

def testArgumentFailure(self): 

-

# Return value 2 means usage problem. 

-

self.assertEqual(self.cog.main(['argv0', '-j']), 2) 

-

output = self.output.getvalue() 

-

self.assertIn("option -j not recognized", output) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0']) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-j']) 

-

 

-

def testNoDashOAndAtFile(self): 

-

d = { 

-

'cogfiles.txt': """\ 

-

# Please run cog 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-o', 'foo', '@cogfiles.txt']) 

-

 

-

def testDashV(self): 

-

self.assertEqual(self.cog.main(['argv0', '-v']), 0) 

-

output = self.output.getvalue() 

-

self.assertEqual('Cog version %s\n' % __version__, output) 

-

 

-

def producesHelp(self, args): 

-

self.newCog() 

-

argv = ['argv0'] + args.split() 

-

self.assertEqual(self.cog.main(argv), 0) 

-

self.assertEqual(usage, self.output.getvalue()) 

-

 

-

def testDashH(self): 

-

# -h or -? anywhere on the command line should just print help. 

-

self.producesHelp("-h") 

-

self.producesHelp("-?") 

-

self.producesHelp("fooey.txt -h") 

-

self.producesHelp("-o -r @fooey.txt -? @booey.txt") 

-

 

-

def testDashOAndDashR(self): 

-

d = { 

-

'cogfile.txt': """\ 

-

# Please run cog 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-o', 'foo', '-r', 'cogfile.txt']) 

-

 

-

def testDashZ(self): 

-

d = { 

-

'test.cog': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

""", 

-

 

-

'test.out': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

void DoSomething(); 

-

void DoAnotherThing(); 

-

void DoLastThing(); 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

with self.assertRaisesRegexp(CogError, re.escape("test.cog(6): Missing '[[[end]]]' before end of file.")): 

-

self.cog.callableMain(['argv0', '-r', 'test.cog']) 

-

self.newCog() 

-

self.cog.callableMain(['argv0', '-r', '-z', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testBadDashD(self): 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-Dfooey', 'cog.txt']) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-D', 'fooey', 'cog.txt']) 

-

 

-

def testBadMarkers(self): 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '--markers=X']) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '--markers=A B C D']) 

-

 

-

 

-

class TestFileHandling(TestCaseWithTempDir): 

-

 

-

def testSimple(self): 

-

d = { 

-

'test.cog': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

void DoSomething(); 

-

void DoAnotherThing(); 

-

void DoLastThing(); 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

output = self.output.getvalue() 

-

self.assertIn("(changed)", output) 

-

 

-

def testOutputFile(self): 

-

# -o sets the output file. 

-

d = { 

-

'test.cog': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

void DoSomething(); 

-

void DoAnotherThing(); 

-

void DoLastThing(); 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-o', 'in/a/dir/test.cogged', 'test.cog']) 

-

self.assertFilesSame('in/a/dir/test.cogged', 'test.out') 

-

 

-

def testAtFile(self): 

-

d = { 

-

'one.cog': """\ 

-

//[[[cog 

-

cog.outl("hello world") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'one.out': """\ 

-

//[[[cog 

-

cog.outl("hello world") 

-

//]]] 

-

hello world 

-

//[[[end]]] 

-

""", 

-

 

-

'two.cog': """\ 

-

//[[[cog 

-

cog.outl("goodbye cruel world") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'two.out': """\ 

-

//[[[cog 

-

cog.outl("goodbye cruel world") 

-

//]]] 

-

goodbye cruel world 

-

//[[[end]]] 

-

""", 

-

 

-

'cogfiles.txt': """\ 

-

# Please run cog 

-

one.cog 

-

 

-

two.cog 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

-

self.assertFilesSame('one.cog', 'one.out') 

-

self.assertFilesSame('two.cog', 'two.out') 

-

output = self.output.getvalue() 

-

self.assertIn("(changed)", output) 

-

 

-

def testNestedAtFile(self): 

-

d = { 

-

'one.cog': """\ 

-

//[[[cog 

-

cog.outl("hello world") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'one.out': """\ 

-

//[[[cog 

-

cog.outl("hello world") 

-

//]]] 

-

hello world 

-

//[[[end]]] 

-

""", 

-

 

-

'two.cog': """\ 

-

//[[[cog 

-

cog.outl("goodbye cruel world") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'two.out': """\ 

-

//[[[cog 

-

cog.outl("goodbye cruel world") 

-

//]]] 

-

goodbye cruel world 

-

//[[[end]]] 

-

""", 

-

 

-

'cogfiles.txt': """\ 

-

# Please run cog 

-

one.cog 

-

@cogfiles2.txt 

-

""", 

-

 

-

'cogfiles2.txt': """\ 

-

# This one too, please. 

-

two.cog 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

-

self.assertFilesSame('one.cog', 'one.out') 

-

self.assertFilesSame('two.cog', 'two.out') 

-

output = self.output.getvalue() 

-

self.assertIn("(changed)", output) 

-

 

-

def testAtFileWithArgs(self): 

-

d = { 

-

'both.cog': """\ 

-

//[[[cog 

-

cog.outl("one: %s" % ('one' in globals())) 

-

cog.outl("two: %s" % ('two' in globals())) 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'one.out': """\ 

-

//[[[cog 

-

cog.outl("one: %s" % ('one' in globals())) 

-

cog.outl("two: %s" % ('two' in globals())) 

-

//]]] 

-

one: True // ONE 

-

two: False // ONE 

-

//[[[end]]] 

-

""", 

-

 

-

'two.out': """\ 

-

//[[[cog 

-

cog.outl("one: %s" % ('one' in globals())) 

-

cog.outl("two: %s" % ('two' in globals())) 

-

//]]] 

-

one: False // TWO 

-

two: True // TWO 

-

//[[[end]]] 

-

""", 

-

 

-

'cogfiles.txt': """\ 

-

# Please run cog 

-

both.cog -o in/a/dir/both.one -s ' // ONE' -D one=x 

-

both.cog -o in/a/dir/both.two -s ' // TWO' -D two=x 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '@cogfiles.txt']) 

-

self.assertFilesSame('in/a/dir/both.one', 'one.out') 

-

self.assertFilesSame('in/a/dir/both.two', 'two.out') 

-

 

-

def testAtFileWithBadArgCombo(self): 

-

d = { 

-

'both.cog': """\ 

-

//[[[cog 

-

cog.outl("one: %s" % ('one' in globals())) 

-

cog.outl("two: %s" % ('two' in globals())) 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'cogfiles.txt': """\ 

-

# Please run cog 

-

both.cog 

-

both.cog -d # This is bad: -r and -d 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

-

 

-

def testAtFileWithTrickyFilenames(self): 

-

def fix_backslashes(files_txt): 

-

"""Make the contents of a files.txt sensitive to the platform.""" 

-

1082 ↛ 1084line 1082 didn't jump to line 1084, because the condition on line 1082 was never false if sys.platform != "win32": 

-

files_txt = files_txt.replace("\\", "/") 

-

return files_txt 

-

 

-

d = { 

-

'one 1.cog': """\ 

-

//[[[cog cog.outl("hello world") ]]] 

-

""", 

-

 

-

'one.out': """\ 

-

//[[[cog cog.outl("hello world") ]]] 

-

hello world //xxx 

-

""", 

-

 

-

'subdir': { 

-

'subback.cog': """\ 

-

//[[[cog cog.outl("down deep with backslashes") ]]] 

-

""", 

-

 

-

'subfwd.cog': """\ 

-

//[[[cog cog.outl("down deep with slashes") ]]] 

-

""", 

-

}, 

-

 

-

'subback.out': """\ 

-

//[[[cog cog.outl("down deep with backslashes") ]]] 

-

down deep with backslashes //yyy 

-

""", 

-

 

-

'subfwd.out': """\ 

-

//[[[cog cog.outl("down deep with slashes") ]]] 

-

down deep with slashes //zzz 

-

""", 

-

 

-

'cogfiles.txt': fix_backslashes("""\ 

-

# Please run cog 

-

'one 1.cog' -s ' //xxx' 

-

subdir\\subback.cog -s ' //yyy' 

-

subdir/subfwd.cog -s ' //zzz' 

-

""") 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-z', '-r', '@cogfiles.txt']) 

-

self.assertFilesSame('one 1.cog', 'one.out') 

-

self.assertFilesSame('subdir/subback.cog', 'subback.out') 

-

self.assertFilesSame('subdir/subfwd.cog', 'subfwd.out') 

-

 

-

def run_with_verbosity(self, verbosity): 

-

d = { 

-

'unchanged.cog': """\ 

-

//[[[cog 

-

cog.outl("hello world") 

-

//]]] 

-

hello world 

-

//[[[end]]] 

-

""", 

-

 

-

'changed.cog': """\ 

-

//[[[cog 

-

cog.outl("goodbye cruel world") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'cogfiles.txt': """\ 

-

unchanged.cog 

-

changed.cog 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '--verbosity='+verbosity, '@cogfiles.txt']) 

-

output = self.output.getvalue() 

-

return output 

-

 

-

def test_verbosity0(self): 

-

output = self.run_with_verbosity("0") 

-

self.assertEqual(output, "") 

-

 

-

def test_verbosity1(self): 

-

output = self.run_with_verbosity("1") 

-

self.assertEqual(output, "Cogging changed.cog (changed)\n") 

-

 

-

def test_verbosity2(self): 

-

output = self.run_with_verbosity("2") 

-

self.assertEqual(output, "Cogging unchanged.cog\nCogging changed.cog (changed)\n") 

-

 

-

 

-

class CogTestLineEndings(TestCaseWithTempDir): 

-

"""Tests for -U option (force LF line-endings in output).""" 

-

 

-

lines_in = ['Some text.', 

-

'//[[[cog', 

-

'cog.outl("Cog text")', 

-

'//]]]', 

-

'gobbledegook.', 

-

'//[[[end]]]', 

-

'epilogue.', 

-

''] 

-

 

-

lines_out = ['Some text.', 

-

'//[[[cog', 

-

'cog.outl("Cog text")', 

-

'//]]]', 

-

'Cog text', 

-

'//[[[end]]]', 

-

'epilogue.', 

-

''] 

-

 

-

def testOutputNativeEol(self): 

-

makeFiles({'infile': '\n'.join(self.lines_in)}) 

-

self.cog.callableMain(['argv0', '-o', 'outfile', 'infile']) 

-

self.assertFileContent('outfile', os.linesep.join(self.lines_out)) 

-

 

-

def testOutputLfEol(self): 

-

makeFiles({'infile': '\n'.join(self.lines_in)}) 

-

self.cog.callableMain(['argv0', '-U', '-o', 'outfile', 'infile']) 

-

self.assertFileContent('outfile', '\n'.join(self.lines_out)) 

-

 

-

def testReplaceNativeEol(self): 

-

makeFiles({'test.cog': '\n'.join(self.lines_in)}) 

-

self.cog.callableMain(['argv0', '-r', 'test.cog']) 

-

self.assertFileContent('test.cog', os.linesep.join(self.lines_out)) 

-

 

-

def testReplaceLfEol(self): 

-

makeFiles({'test.cog': '\n'.join(self.lines_in)}) 

-

self.cog.callableMain(['argv0', '-U', '-r', 'test.cog']) 

-

self.assertFileContent('test.cog', '\n'.join(self.lines_out)) 

-

 

-

 

-

class CogTestCharacterEncoding(TestCaseWithTempDir): 

-

 

-

def testSimple(self): 

-

d = { 

-

'test.cog': b("""\ 

-

// This is my C++ file. 

-

//[[[cog 

-

cog.outl("// Unicode: \xe1\x88\xb4 (U+1234)") 

-

//]]] 

-

//[[[end]]] 

-

"""), 

-

 

-

'test.out': b("""\ 

-

// This is my C++ file. 

-

//[[[cog 

-

cog.outl("// Unicode: \xe1\x88\xb4 (U+1234)") 

-

//]]] 

-

// Unicode: \xe1\x88\xb4 (U+1234) 

-

//[[[end]]] 

-

"""), 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

output = self.output.getvalue() 

-

self.assertIn("(changed)", output) 

-

 

-

def testFileEncodingOption(self): 

-

d = { 

-

'test.cog': b("""\ 

-

// \xca\xee\xe4\xe8\xf0\xe2\xea\xe0 Windows 

-

//[[[cog 

-

cog.outl("\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe") 

-

//]]] 

-

//[[[end]]] 

-

"""), 

-

 

-

'test.out': b("""\ 

-

// \xca\xee\xe4\xe8\xf0\xe2\xea\xe0 Windows 

-

//[[[cog 

-

cog.outl("\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe") 

-

//]]] 

-

\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe 

-

//[[[end]]] 

-

"""), 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-n', 'cp1251', '-r', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

output = self.output.getvalue() 

-

self.assertIn("(changed)", output) 

-

 

-

 

-

class TestCaseWithImports(TestCaseWithTempDir): 

-

""" When running tests which import modules, the sys.modules list 

-

leaks from one test to the next. This test case class scrubs 

-

the list after each run to keep the tests isolated from each other. 

-

""" 

-

 

-

def setUp(self): 

-

TestCaseWithTempDir.setUp(self) 

-

self.sysmodulekeys = list(sys.modules) 

-

 

-

def tearDown(self): 

-

modstoscrub = [ 

-

modname 

-

for modname in sys.modules 

-

if modname not in self.sysmodulekeys 

-

] 

-

for modname in modstoscrub: 

-

del sys.modules[modname] 

-

TestCaseWithTempDir.tearDown(self) 

-

 

-

 

-

class CogIncludeTests(TestCaseWithImports): 

-

dincludes = { 

-

'test.cog': """\ 

-

//[[[cog 

-

import mymodule 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

//[[[cog 

-

import mymodule 

-

//]]] 

-

Hello from mymodule 

-

//[[[end]]] 

-

""", 

-

 

-

'test2.out': """\ 

-

//[[[cog 

-

import mymodule 

-

//]]] 

-

Hello from mymodule in inc2 

-

//[[[end]]] 

-

""", 

-

 

-

'include': { 

-

'mymodule.py': """\ 

-

import cog 

-

cog.outl("Hello from mymodule") 

-

""" 

-

}, 

-

 

-

'inc2': { 

-

'mymodule.py': """\ 

-

import cog 

-

cog.outl("Hello from mymodule in inc2") 

-

""" 

-

}, 

-

 

-

'inc3': { 

-

'someothermodule.py': """\ 

-

import cog 

-

cog.outl("This is some other module.") 

-

""" 

-

}, 

-

} 

-

 

-

def testNeedIncludePath(self): 

-

# Try it without the -I, to see that an ImportError happens. 

-

makeFiles(self.dincludes) 

-

with self.assertRaises(ImportError): 

-

self.cog.callableMain(['argv0', '-r', 'test.cog']) 

-

 

-

def testIncludePath(self): 

-

# Test that -I adds include directories properly. 

-

makeFiles(self.dincludes) 

-

self.cog.callableMain(['argv0', '-r', '-I', 'include', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testTwoIncludePaths(self): 

-

# Test that two -I's add include directories properly. 

-

makeFiles(self.dincludes) 

-

self.cog.callableMain(['argv0', '-r', '-I', 'include', '-I', 'inc2', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testTwoIncludePaths2(self): 

-

# Test that two -I's add include directories properly. 

-

makeFiles(self.dincludes) 

-

self.cog.callableMain(['argv0', '-r', '-I', 'inc2', '-I', 'include', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test2.out') 

-

 

-

def testUselessIncludePath(self): 

-

# Test that the search will continue past the first directory. 

-

makeFiles(self.dincludes) 

-

self.cog.callableMain(['argv0', '-r', '-I', 'inc3', '-I', 'include', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testSysPathIsUnchanged(self): 

-

d = { 

-

'bad.cog': """\ 

-

//[[[cog cog.error("Oh no!") ]]] 

-

//[[[end]]] 

-

""", 

-

'good.cog': """\ 

-

//[[[cog cog.outl("Oh yes!") ]]] 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

# Is it unchanged just by creating a cog engine? 

-

oldsyspath = sys.path[:] 

-

self.newCog() 

-

self.assertEqual(oldsyspath, sys.path) 

-

# Is it unchanged for a successful run? 

-

self.newCog() 

-

self.cog.callableMain(['argv0', '-r', 'good.cog']) 

-

self.assertEqual(oldsyspath, sys.path) 

-

# Is it unchanged for a successful run with includes? 

-

self.newCog() 

-

self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', 'good.cog']) 

-

self.assertEqual(oldsyspath, sys.path) 

-

# Is it unchanged for a successful run with two includes? 

-

self.newCog() 

-

self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', '-I', 'quux', 'good.cog']) 

-

self.assertEqual(oldsyspath, sys.path) 

-

# Is it unchanged for a failed run? 

-

self.newCog() 

-

with self.assertRaises(CogError): 

-

self.cog.callableMain(['argv0', '-r', 'bad.cog']) 

-

self.assertEqual(oldsyspath, sys.path) 

-

# Is it unchanged for a failed run with includes? 

-

self.newCog() 

-

with self.assertRaises(CogError): 

-

self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', 'bad.cog']) 

-

self.assertEqual(oldsyspath, sys.path) 

-

# Is it unchanged for a failed run with two includes? 

-

self.newCog() 

-

with self.assertRaises(CogError): 

-

self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', '-I', 'quux', 'bad.cog']) 

-

self.assertEqual(oldsyspath, sys.path) 

-

 

-

def testSubDirectories(self): 

-

# Test that relative paths on the command line work, with includes. 

-

 

-

d = { 

-

'code': { 

-

'test.cog': """\ 

-

//[[[cog 

-

import mysubmodule 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

//[[[cog 

-

import mysubmodule 

-

//]]] 

-

Hello from mysubmodule 

-

//[[[end]]] 

-

""", 

-

 

-

'mysubmodule.py': """\ 

-

import cog 

-

cog.outl("Hello from mysubmodule") 

-

""" 

-

} 

-

} 

-

 

-

makeFiles(d) 

-

# We should be able to invoke cog without the -I switch, and it will 

-

# auto-include the current directory 

-

self.cog.callableMain(['argv0', '-r', 'code/test.cog']) 

-

self.assertFilesSame('code/test.cog', 'code/test.out') 

-

 

-

 

-

class CogTestsInFiles(TestCaseWithTempDir): 

-

 

-

def testWarnIfNoCogCode(self): 

-

# Test that the -e switch warns if there is no Cog code. 

-

d = { 

-

'with.cog': """\ 

-

//[[[cog 

-

cog.outl("hello world") 

-

//]]] 

-

hello world 

-

//[[[end]]] 

-

""", 

-

 

-

'without.cog': """\ 

-

There's no cog 

-

code in this file. 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-e', 'with.cog']) 

-

output = self.output.getvalue() 

-

self.assertNotIn("Warning", output) 

-

self.newCog() 

-

self.cog.callableMain(['argv0', '-e', 'without.cog']) 

-

output = self.output.getvalue() 

-

self.assertIn("Warning: no cog code found in without.cog", output) 

-

self.newCog() 

-

self.cog.callableMain(['argv0', 'without.cog']) 

-

output = self.output.getvalue() 

-

self.assertNotIn("Warning", output) 

-

 

-

def testFileNameProps(self): 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) 

-

//]]] 

-

this is cog1.txt in, cog1.txt out 

-

[[[end]]] 

-

""", 

-

 

-

'cog1.out': """\ 

-

//[[[cog 

-

cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) 

-

//]]] 

-

This is cog1.txt in, cog1.txt out 

-

[[[end]]] 

-

""", 

-

 

-

'cog1out.out': """\ 

-

//[[[cog 

-

cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) 

-

//]]] 

-

This is cog1.txt in, cog1out.txt out 

-

[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out') 

-

self.newCog() 

-

self.cog.callableMain(['argv0', '-o', 'cog1out.txt', 'cog1.txt']) 

-

self.assertFilesSame('cog1out.txt', 'cog1out.out') 

-

 

-

def testGlobalsDontCrossFiles(self): 

-

# Make sure that global values don't get shared between files. 

-

d = { 

-

'one.cog': """\ 

-

//[[[cog s = "This was set in one.cog" ]]] 

-

//[[[end]]] 

-

//[[[cog cog.outl(s) ]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'one.out': """\ 

-

//[[[cog s = "This was set in one.cog" ]]] 

-

//[[[end]]] 

-

//[[[cog cog.outl(s) ]]] 

-

This was set in one.cog 

-

//[[[end]]] 

-

""", 

-

 

-

'two.cog': """\ 

-

//[[[cog 

-

try: 

-

cog.outl(s) 

-

except NameError: 

-

cog.outl("s isn't set!") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'two.out': """\ 

-

//[[[cog 

-

try: 

-

cog.outl(s) 

-

except NameError: 

-

cog.outl("s isn't set!") 

-

//]]] 

-

s isn't set! 

-

//[[[end]]] 

-

""", 

-

 

-

'cogfiles.txt': """\ 

-

# Please run cog 

-

one.cog 

-

 

-

two.cog 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

-

self.assertFilesSame('one.cog', 'one.out') 

-

self.assertFilesSame('two.cog', 'two.out') 

-

output = self.output.getvalue() 

-

self.assertIn("(changed)", output) 

-

 

-

def testRemoveGeneratedOutput(self): 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was generated.") 

-

//]]] 

-

This line was generated. 

-

//[[[end]]] 

-

This line was not. 

-

""", 

-

 

-

'cog1.out': """\ 

-

//[[[cog 

-

cog.outl("This line was generated.") 

-

//]]] 

-

//[[[end]]] 

-

This line was not. 

-

""", 

-

 

-

'cog1.out2': """\ 

-

//[[[cog 

-

cog.outl("This line was generated.") 

-

//]]] 

-

This line was generated. 

-

//[[[end]]] 

-

This line was not. 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

# Remove generated output. 

-

self.cog.callableMain(['argv0', '-r', '-x', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out') 

-

self.newCog() 

-

# Regenerate the generated output. 

-

self.cog.callableMain(['argv0', '-r', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out2') 

-

self.newCog() 

-

# Remove the generated output again. 

-

self.cog.callableMain(['argv0', '-r', '-x', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out') 

-

 

-

def testMsgCall(self): 

-

infile = """\ 

-

#[[[cog 

-

cog.msg("Hello there!") 

-

#]]] 

-

#[[[end]]] 

-

""" 

-

infile = reindentBlock(infile) 

-

self.assertEqual(self.cog.processString(infile), infile) 

-

output = self.output.getvalue() 

-

self.assertEqual(output, "Message: Hello there!\n") 

-

 

-

def testErrorMessageHasNoTraceback(self): 

-

# Test that a Cog error is printed to stderr with no traceback. 

-

 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

Xhis line was newly 

-

generated by cog 

-

blah blah. 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

stderr = StringIO() 

-

self.cog.setOutput(stderr=stderr) 

-

self.cog.main(['argv0', '-c', '-r', "cog1.txt"]) 

-

self.assertEqual(self.output.getvalue(), "Cogging cog1.txt\n") 

-

self.assertEqual(stderr.getvalue(), "cog1.txt(9): Output has been edited! Delete old checksum to unprotect.\n") 

-

 

-

def testDashD(self): 

-

d = { 

-

'test.cog': """\ 

-

--[[[cog cog.outl("Defined fooey as " + fooey) ]]] 

-

--[[[end]]] 

-

""", 

-

 

-

'test.kablooey': """\ 

-

--[[[cog cog.outl("Defined fooey as " + fooey) ]]] 

-

Defined fooey as kablooey 

-

--[[[end]]] 

-

""", 

-

 

-

'test.einstein': """\ 

-

--[[[cog cog.outl("Defined fooey as " + fooey) ]]] 

-

Defined fooey as e=mc2 

-

--[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-D', 'fooey=kablooey', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.kablooey') 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-Dfooey=kablooey', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.kablooey') 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-Dfooey=e=mc2', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.einstein') 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-Dbar=quux', '-Dfooey=kablooey', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.kablooey') 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-Dfooey=kablooey', '-Dbar=quux', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.kablooey') 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-Dfooey=gooey', '-Dfooey=kablooey', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.kablooey') 

-

 

-

def testOutputToStdout(self): 

-

d = { 

-

'test.cog': """\ 

-

--[[[cog cog.outl('Hey there!') ]]] 

-

--[[[end]]] 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

stderr = StringIO() 

-

self.cog.setOutput(stderr=stderr) 

-

self.cog.callableMain(['argv0', 'test.cog']) 

-

output = self.output.getvalue() 

-

outerr = stderr.getvalue() 

-

self.assertEqual(output, "--[[[cog cog.outl('Hey there!') ]]]\nHey there!\n--[[[end]]]\n") 

-

self.assertEqual(outerr, "") 

-

 

-

def testReadFromStdin(self): 

-

stdin = StringIO("--[[[cog cog.outl('Wow') ]]]\n--[[[end]]]\n") 

-

def restore_stdin(old_stdin): 

-

sys.stdin = old_stdin 

-

self.addCleanup(restore_stdin, sys.stdin) 

-

sys.stdin = stdin 

-

 

-

stderr = StringIO() 

-

self.cog.setOutput(stderr=stderr) 

-

self.cog.callableMain(['argv0', '-']) 

-

output = self.output.getvalue() 

-

outerr = stderr.getvalue() 

-

self.assertEqual(output, "--[[[cog cog.outl('Wow') ]]]\nWow\n--[[[end]]]\n") 

-

self.assertEqual(outerr, "") 

-

 

-

 

-

def testSuffixOutputLines(self): 

-

d = { 

-

'test.cog': """\ 

-

Hey there. 

-

;[[[cog cog.outl('a\\nb\\n \\nc') ]]] 

-

;[[[end]]] 

-

Good bye. 

-

""", 

-

 

-

'test.out': """\ 

-

Hey there. 

-

;[[[cog cog.outl('a\\nb\\n \\nc') ]]] 

-

a (foo) 

-

b (foo) 

-

""" # These three trailing spaces are important. 

-

# The suffix is not applied to completely blank lines. 

-

""" 

-

c (foo) 

-

;[[[end]]] 

-

Good bye. 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-s', ' (foo)', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testEmptySuffix(self): 

-

d = { 

-

'test.cog': """\ 

-

;[[[cog cog.outl('a\\nb\\nc') ]]] 

-

;[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

;[[[cog cog.outl('a\\nb\\nc') ]]] 

-

a 

-

b 

-

c 

-

;[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-s', '', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testHellishSuffix(self): 

-

d = { 

-

'test.cog': """\ 

-

;[[[cog cog.outl('a\\n\\nb') ]]] 

-

""", 

-

 

-

'test.out': """\ 

-

;[[[cog cog.outl('a\\n\\nb') ]]] 

-

a /\\n*+([)]>< 

-

 

-

b /\\n*+([)]>< 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-z', '-r', '-s', r' /\n*+([)]><', 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

 

-

class WritabilityTests(TestCaseWithTempDir): 

-

 

-

d = { 

-

'test.cog': """\ 

-

//[[[cog 

-

for fn in ['DoSomething', 'DoAnotherThing', 'DoLastThing']: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

//[[[cog 

-

for fn in ['DoSomething', 'DoAnotherThing', 'DoLastThing']: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

void DoSomething(); 

-

void DoAnotherThing(); 

-

void DoLastThing(); 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

if os.name == 'nt': #pragma: no cover 

-

# for Windows 

-

cmd_w_args = 'attrib -R %s' 

-

cmd_w_asterisk = 'attrib -R *' 

-

else: #pragma: no cover 

-

# for unix-like 

-

cmd_w_args = 'chmod +w %s' 

-

cmd_w_asterisk = 'chmod +w *' 

-

 

-

def setUp(self): 

-

TestCaseWithTempDir.setUp(self) 

-

makeFiles(self.d) 

-

self.testcog = os.path.join(self.tempdir, 'test.cog') 

-

os.chmod(self.testcog, stat.S_IREAD) # Make the file readonly. 

-

assert not os.access(self.testcog, os.W_OK) 

-

 

-

def tearDown(self): 

-

os.chmod(self.testcog, stat.S_IWRITE) # Make the file writable again. 

-

TestCaseWithTempDir.tearDown(self) 

-

 

-

def testReadonlyNoCommand(self): 

-

with self.assertRaisesRegexp(CogError, "Can't overwrite test.cog"): 

-

self.cog.callableMain(['argv0', '-r', 'test.cog']) 

-

assert not os.access(self.testcog, os.W_OK) 

-

 

-

def testReadonlyWithCommand(self): 

-

self.cog.callableMain(['argv0', '-r', '-w', self.cmd_w_args, 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

assert os.access(self.testcog, os.W_OK) 

-

 

-

def testReadonlyWithCommandWithNoSlot(self): 

-

self.cog.callableMain(['argv0', '-r', '-w', self.cmd_w_asterisk, 'test.cog']) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

assert os.access(self.testcog, os.W_OK) 

-

 

-

def testReadonlyWithIneffectualCommand(self): 

-

with self.assertRaisesRegexp(CogError, "Couldn't make test.cog writable"): 

-

self.cog.callableMain(['argv0', '-r', '-w', 'echo %s', 'test.cog']) 

-

assert not os.access(self.testcog, os.W_OK) 

-

 

-

 

-

class ChecksumTests(TestCaseWithTempDir): 

-

 

-

def testCreateChecksumOutput(self): 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was generated.") 

-

//]]] 

-

This line was generated. 

-

//[[[end]]] 

-

This line was not. 

-

""", 

-

 

-

'cog1.out': """\ 

-

//[[[cog 

-

cog.outl("This line was generated.") 

-

//]]] 

-

This line was generated. 

-

//[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) 

-

This line was not. 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-c', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out') 

-

 

-

def testCheckChecksumOutput(self): 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was generated. 

-

//[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) 

-

""", 

-

 

-

'cog1.out': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was newly 

-

generated by cog 

-

blah blah. 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', '-c', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out') 

-

 

-

def testRemoveChecksumOutput(self): 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was generated. 

-

//[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) fooey 

-

""", 

-

 

-

'cog1.out': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was newly 

-

generated by cog 

-

blah blah. 

-

//[[[end]]] fooey 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-r', 'cog1.txt']) 

-

self.assertFilesSame('cog1.txt', 'cog1.out') 

-

 

-

def testTamperedChecksumOutput(self): 

-

d = { 

-

'cog1.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

Xhis line was newly 

-

generated by cog 

-

blah blah. 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

 

-

'cog2.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was newly 

-

generated by cog 

-

blah blah! 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

 

-

'cog3.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

 

-

This line was newly 

-

generated by cog 

-

blah blah. 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

 

-

'cog4.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was newly 

-

generated by cog 

-

blah blah.. 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

 

-

'cog5.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

This line was newly 

-

generated by cog 

-

blah blah. 

-

extra 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

 

-

'cog6.txt': """\ 

-

//[[[cog 

-

cog.outl("This line was newly") 

-

cog.outl("generated by cog") 

-

cog.outl("blah blah.") 

-

//]]] 

-

//[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

with self.assertRaisesRegexp(CogError, 

-

r"cog1.txt\(9\): Output has been edited! Delete old checksum to unprotect."): 

-

self.cog.callableMain(['argv0', '-c', "cog1.txt"]) 

-

with self.assertRaisesRegexp(CogError, 

-

r"cog2.txt\(9\): Output has been edited! Delete old checksum to unprotect."): 

-

self.cog.callableMain(['argv0', '-c', "cog2.txt"]) 

-

with self.assertRaisesRegexp(CogError, 

-

r"cog3.txt\(10\): Output has been edited! Delete old checksum to unprotect."): 

-

self.cog.callableMain(['argv0', '-c', "cog3.txt"]) 

-

with self.assertRaisesRegexp(CogError, 

-

r"cog4.txt\(9\): Output has been edited! Delete old checksum to unprotect."): 

-

self.cog.callableMain(['argv0', '-c', "cog4.txt"]) 

-

with self.assertRaisesRegexp(CogError, 

-

r"cog5.txt\(10\): Output has been edited! Delete old checksum to unprotect."): 

-

self.cog.callableMain(['argv0', '-c', "cog5.txt"]) 

-

with self.assertRaisesRegexp(CogError, 

-

r"cog6.txt\(6\): Output has been edited! Delete old checksum to unprotect."): 

-

self.cog.callableMain(['argv0', '-c', "cog6.txt"]) 

-

 

-

def testArgvIsntModified(self): 

-

argv = ['argv0', '-v'] 

-

orig_argv = argv[:] 

-

self.cog.callableMain(argv) 

-

self.assertEqual(argv, orig_argv) 

-

 

-

 

-

class CustomMarkerTests(TestCaseWithTempDir): 

-

 

-

def testCustomerMarkers(self): 

-

d = { 

-

'test.cog': """\ 

-

//{{ 

-

cog.outl("void %s();" % "MyFunction") 

-

//}} 

-

//{{end}} 

-

""", 

-

 

-

'test.out': """\ 

-

//{{ 

-

cog.outl("void %s();" % "MyFunction") 

-

//}} 

-

void MyFunction(); 

-

//{{end}} 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain([ 

-

'argv0', '-r', 

-

'--markers={{ }} {{end}}', 

-

'test.cog' 

-

]) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testTrulyWackyMarkers(self): 

-

# Make sure the markers are properly re-escaped. 

-

d = { 

-

'test.cog': """\ 

-

//**( 

-

cog.outl("void %s();" % "MyFunction") 

-

//**) 

-

//**(end)** 

-

""", 

-

 

-

'test.out': """\ 

-

//**( 

-

cog.outl("void %s();" % "MyFunction") 

-

//**) 

-

void MyFunction(); 

-

//**(end)** 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain([ 

-

'argv0', '-r', 

-

'--markers=**( **) **(end)**', 

-

'test.cog' 

-

]) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

def testChangeJustOneMarker(self): 

-

d = { 

-

'test.cog': """\ 

-

//**( 

-

cog.outl("void %s();" % "MyFunction") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

 

-

'test.out': """\ 

-

//**( 

-

cog.outl("void %s();" % "MyFunction") 

-

//]]] 

-

void MyFunction(); 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain([ 

-

'argv0', '-r', 

-

'--markers=**( ]]] [[[end]]]', 

-

'test.cog' 

-

]) 

-

self.assertFilesSame('test.cog', 'test.out') 

-

 

-

 

-

class BlakeTests(TestCaseWithTempDir): 

-

 

-

# Blake Winton's contributions. 

-

def testDeleteCode(self): 

-

# -o sets the output file. 

-

d = { 

-

'test.cog': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

Some Sample Code Here 

-

//[[[end]]]Data Data 

-

And Some More 

-

""", 

-

 

-

'test.out': """\ 

-

// This is my C++ file. 

-

void DoSomething(); 

-

void DoAnotherThing(); 

-

void DoLastThing(); 

-

And Some More 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.callableMain(['argv0', '-d', '-o', 'test.cogged', 'test.cog']) 

-

self.assertFilesSame('test.cogged', 'test.out') 

-

 

-

def testDeleteCodeWithDashRFails(self): 

-

d = { 

-

'test.cog': """\ 

-

// This is my C++ file. 

-

""" 

-

} 

-

 

-

makeFiles(d) 

-

with self.assertRaises(CogUsageError): 

-

self.cog.callableMain(['argv0', '-r', '-d', 'test.cog']) 

-

 

-

def testSettingGlobals(self): 

-

# Blake Winton contributed a way to set the globals that will be used in 

-

# processFile(). 

-

d = { 

-

'test.cog': """\ 

-

// This is my C++ file. 

-

//[[[cog 

-

for fn in fnames: 

-

cog.outl("void %s();" % fn) 

-

//]]] 

-

Some Sample Code Here 

-

//[[[end]]]""", 

-

 

-

'test.out': """\ 

-

// This is my C++ file. 

-

void DoBlake(); 

-

void DoWinton(); 

-

void DoContribution(); 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

globals = {} 

-

globals['fnames'] = ['DoBlake', 'DoWinton', 'DoContribution'] 

-

self.cog.options.bDeleteCode = True 

-

self.cog.processFile('test.cog', 'test.cogged', globals=globals) 

-

self.assertFilesSame('test.cogged', 'test.out') 

-

 

-

 

-

class ErrorCallTests(TestCaseWithTempDir): 

-

 

-

def testErrorCallHasNoTraceback(self): 

-

# Test that cog.error() doesn't show a traceback. 

-

d = { 

-

'error.cog': """\ 

-

//[[[cog 

-

cog.error("Something Bad!") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.main(['argv0', '-r', 'error.cog']) 

-

output = self.output.getvalue() 

-

self.assertEqual(output, "Cogging error.cog\nError: Something Bad!\n") 

-

 

-

def testRealErrorHasTraceback(self): 

-

# Test that a genuine error does show a traceback. 

-

d = { 

-

'error.cog': """\ 

-

//[[[cog 

-

raise RuntimeError("Hey!") 

-

//]]] 

-

//[[[end]]] 

-

""", 

-

} 

-

 

-

makeFiles(d) 

-

self.cog.main(['argv0', '-r', 'error.cog']) 

-

output = self.output.getvalue() 

-

msg = 'Actual output:\n' + output 

-

self.assert_(output.startswith("Cogging error.cog\nTraceback (most recent"), msg) 

-

self.assertIn("RuntimeError: Hey!", output) 

-

 

-

 

-

# Things not yet tested: 

-

# - A bad -w command (currently fails silently). 

- -
+

1""" Test cogapp. 

+

2 http://nedbatchelder.com/code/cog 

+

3 

+

4 Copyright 2004-2019, Ned Batchelder. 

+

5""" 

+

6 

+

7from __future__ import absolute_import 

+

8 

+

9import os 

+

10import os.path 

+

11import random 

+

12import re 

+

13import shutil 

+

14import stat 

+

15import sys 

+

16import tempfile 

+

17import threading 

+

18 

+

19from .backward import StringIO, to_bytes, TestCase, PY3 

+

20from .cogapp import Cog, CogOptions, CogGenerator 

+

21from .cogapp import CogError, CogUsageError, CogGeneratedError, CogUserException 

+

22from .cogapp import usage, __version__, main 

+

23from .makefiles import * 

+

24from .whiteutils import reindentBlock 

+

25 

+

26 

+

27class CogTestsInMemory(TestCase): 

+

28 """ Test cases for cogapp.Cog() 

+

29 """ 

+

30 

+

31 def testNoCog(self): 

+

32 strings = [ 

+

33 '', 

+

34 ' ', 

+

35 ' \t \t \tx', 

+

36 'hello', 

+

37 'the cat\nin the\nhat.', 

+

38 'Horton\n\tHears A\n\t\tWho' 

+

39 ] 

+

40 for s in strings: 

+

41 self.assertEqual(Cog().processString(s), s) 

+

42 

+

43 def testSimple(self): 

+

44 infile = """\ 

+

45 Some text. 

+

46 //[[[cog 

+

47 import cog 

+

48 cog.outl("This is line one\\n") 

+

49 cog.outl("This is line two") 

+

50 //]]] 

+

51 gobbledegook. 

+

52 //[[[end]]] 

+

53 epilogue. 

+

54 """ 

+

55 

+

56 outfile = """\ 

+

57 Some text. 

+

58 //[[[cog 

+

59 import cog 

+

60 cog.outl("This is line one\\n") 

+

61 cog.outl("This is line two") 

+

62 //]]] 

+

63 This is line one 

+

64 

+

65 This is line two 

+

66 //[[[end]]] 

+

67 epilogue. 

+

68 """ 

+

69 

+

70 self.assertEqual(Cog().processString(infile), outfile) 

+

71 

+

72 def testEmptyCog(self): 

+

73 # The cog clause can be totally empty. Not sure why you'd want it, 

+

74 # but it works. 

+

75 infile = """\ 

+

76 hello 

+

77 //[[[cog 

+

78 //]]] 

+

79 //[[[end]]] 

+

80 goodbye 

+

81 """ 

+

82 

+

83 infile = reindentBlock(infile) 

+

84 self.assertEqual(Cog().processString(infile), infile) 

+

85 

+

86 def testMultipleCogs(self): 

+

87 # One file can have many cog chunks, even abutting each other. 

+

88 infile = """\ 

+

89 //[[[cog 

+

90 cog.out("chunk1") 

+

91 //]]] 

+

92 chunk1 

+

93 //[[[end]]] 

+

94 //[[[cog 

+

95 cog.out("chunk2") 

+

96 //]]] 

+

97 chunk2 

+

98 //[[[end]]] 

+

99 between chunks 

+

100 //[[[cog 

+

101 cog.out("chunk3") 

+

102 //]]] 

+

103 chunk3 

+

104 //[[[end]]] 

+

105 """ 

+

106 

+

107 infile = reindentBlock(infile) 

+

108 self.assertEqual(Cog().processString(infile), infile) 

+

109 

+

110 def testTrimBlankLines(self): 

+

111 infile = """\ 

+

112 //[[[cog 

+

113 cog.out("This is line one\\n", trimblanklines=True) 

+

114 cog.out(''' 

+

115 This is line two 

+

116 ''', dedent=True, trimblanklines=True) 

+

117 cog.outl("This is line three", trimblanklines=True) 

+

118 //]]] 

+

119 This is line one 

+

120 This is line two 

+

121 This is line three 

+

122 //[[[end]]] 

+

123 """ 

+

124 

+

125 infile = reindentBlock(infile) 

+

126 self.assertEqual(Cog().processString(infile), infile) 

+

127 

+

128 def testTrimEmptyBlankLines(self): 

+

129 infile = """\ 

+

130 //[[[cog 

+

131 cog.out("This is line one\\n", trimblanklines=True) 

+

132 cog.out(''' 

+

133 This is line two 

+

134 ''', dedent=True, trimblanklines=True) 

+

135 cog.out('', dedent=True, trimblanklines=True) 

+

136 cog.outl("This is line three", trimblanklines=True) 

+

137 //]]] 

+

138 This is line one 

+

139 This is line two 

+

140 This is line three 

+

141 //[[[end]]] 

+

142 """ 

+

143 

+

144 infile = reindentBlock(infile) 

+

145 self.assertEqual(Cog().processString(infile), infile) 

+

146 

+

147 def testTrimBlankLinesWithLastPartial(self): 

+

148 infile = """\ 

+

149 //[[[cog 

+

150 cog.out("This is line one\\n", trimblanklines=True) 

+

151 cog.out("\\nLine two\\nLine three", trimblanklines=True) 

+

152 //]]] 

+

153 This is line one 

+

154 Line two 

+

155 Line three 

+

156 //[[[end]]] 

+

157 """ 

+

158 

+

159 infile = reindentBlock(infile) 

+

160 self.assertEqual(Cog().processString(infile), infile) 

+

161 

+

162 def testCogOutDedent(self): 

+

163 infile = """\ 

+

164 //[[[cog 

+

165 cog.out("This is the first line\\n") 

+

166 cog.out(''' 

+

167 This is dedent=True 1 

+

168 This is dedent=True 2 

+

169 ''', dedent=True, trimblanklines=True) 

+

170 cog.out(''' 

+

171 This is dedent=False 1 

+

172 This is dedent=False 2 

+

173 ''', dedent=False, trimblanklines=True) 

+

174 cog.out(''' 

+

175 This is dedent=default 1 

+

176 This is dedent=default 2 

+

177 ''', trimblanklines=True) 

+

178 cog.out("This is the last line\\n") 

+

179 //]]] 

+

180 This is the first line 

+

181 This is dedent=True 1 

+

182 This is dedent=True 2 

+

183 This is dedent=False 1 

+

184 This is dedent=False 2 

+

185 This is dedent=default 1 

+

186 This is dedent=default 2 

+

187 This is the last line 

+

188 //[[[end]]] 

+

189 """ 

+

190 

+

191 infile = reindentBlock(infile) 

+

192 self.assertEqual(Cog().processString(infile), infile) 

+

193 

+

194 def test22EndOfLine(self): 

+

195 # In Python 2.2, this cog file was not parsing because the 

+

196 # last line is indented but didn't end with a newline. 

+

197 infile = """\ 

+

198 //[[[cog 

+

199 import cog 

+

200 for i in range(3): 

+

201 cog.out("%d\\n" % i) 

+

202 //]]] 

+

203 0 

+

204 1 

+

205 2 

+

206 //[[[end]]] 

+

207 """ 

+

208 

+

209 infile = reindentBlock(infile) 

+

210 self.assertEqual(Cog().processString(infile), infile) 

+

211 

+

212 def testIndentedCode(self): 

+

213 infile = """\ 

+

214 first line 

+

215 [[[cog 

+

216 import cog 

+

217 for i in range(3): 

+

218 cog.out("xx%d\\n" % i) 

+

219 ]]] 

+

220 xx0 

+

221 xx1 

+

222 xx2 

+

223 [[[end]]] 

+

224 last line 

+

225 """ 

+

226 

+

227 infile = reindentBlock(infile) 

+

228 self.assertEqual(Cog().processString(infile), infile) 

+

229 

+

230 def testPrefixedCode(self): 

+

231 infile = """\ 

+

232 --[[[cog 

+

233 --import cog 

+

234 --for i in range(3): 

+

235 -- cog.out("xx%d\\n" % i) 

+

236 --]]] 

+

237 xx0 

+

238 xx1 

+

239 xx2 

+

240 --[[[end]]] 

+

241 """ 

+

242 

+

243 infile = reindentBlock(infile) 

+

244 self.assertEqual(Cog().processString(infile), infile) 

+

245 

+

246 def testPrefixedIndentedCode(self): 

+

247 infile = """\ 

+

248 prologue 

+

249 --[[[cog 

+

250 -- import cog 

+

251 -- for i in range(3): 

+

252 -- cog.out("xy%d\\n" % i) 

+

253 --]]] 

+

254 xy0 

+

255 xy1 

+

256 xy2 

+

257 --[[[end]]] 

+

258 """ 

+

259 

+

260 infile = reindentBlock(infile) 

+

261 self.assertEqual(Cog().processString(infile), infile) 

+

262 

+

263 def testBogusPrefixMatch(self): 

+

264 infile = """\ 

+

265 prologue 

+

266 #[[[cog 

+

267 import cog 

+

268 # This comment should not be clobbered by removing the pound sign. 

+

269 for i in range(3): 

+

270 cog.out("xy%d\\n" % i) 

+

271 #]]] 

+

272 xy0 

+

273 xy1 

+

274 xy2 

+

275 #[[[end]]] 

+

276 """ 

+

277 

+

278 infile = reindentBlock(infile) 

+

279 self.assertEqual(Cog().processString(infile), infile) 

+

280 

+

281 def testNoFinalNewline(self): 

+

282 # If the cog'ed output has no final newline, 

+

283 # it shouldn't eat up the cog terminator. 

+

284 infile = """\ 

+

285 prologue 

+

286 [[[cog 

+

287 import cog 

+

288 for i in range(3): 

+

289 cog.out("%d" % i) 

+

290 ]]] 

+

291 012 

+

292 [[[end]]] 

+

293 epilogue 

+

294 """ 

+

295 

+

296 infile = reindentBlock(infile) 

+

297 self.assertEqual(Cog().processString(infile), infile) 

+

298 

+

299 def testNoOutputAtAll(self): 

+

300 # If there is absolutely no cog output, that's ok. 

+

301 infile = """\ 

+

302 prologue 

+

303 [[[cog 

+

304 i = 1 

+

305 ]]] 

+

306 [[[end]]] 

+

307 epilogue 

+

308 """ 

+

309 

+

310 infile = reindentBlock(infile) 

+

311 self.assertEqual(Cog().processString(infile), infile) 

+

312 

+

313 def testPurelyBlankLine(self): 

+

314 # If there is a blank line in the cog code with no whitespace 

+

315 # prefix, that should be OK. 

+

316 

+

317 infile = """\ 

+

318 prologue 

+

319 [[[cog 

+

320 import sys 

+

321 cog.out("Hello") 

+

322 $ 

+

323 cog.out("There") 

+

324 ]]] 

+

325 HelloThere 

+

326 [[[end]]] 

+

327 epilogue 

+

328 """ 

+

329 

+

330 infile = reindentBlock(infile.replace('$', '')) 

+

331 self.assertEqual(Cog().processString(infile), infile) 

+

332 

+

333 def testEmptyOutl(self): 

+

334 # Alexander Belchenko suggested the string argument to outl should 

+

335 # be optional. Does it work? 

+

336 

+

337 infile = """\ 

+

338 prologue 

+

339 [[[cog 

+

340 cog.outl("x") 

+

341 cog.outl() 

+

342 cog.outl("y") 

+

343 cog.out() # Also optional, a complete no-op. 

+

344 cog.outl(trimblanklines=True) 

+

345 cog.outl("z") 

+

346 ]]] 

+

347 x 

+

348 

+

349 y 

+

350 

+

351 z 

+

352 [[[end]]] 

+

353 epilogue 

+

354 """ 

+

355 

+

356 infile = reindentBlock(infile) 

+

357 self.assertEqual(Cog().processString(infile), infile) 

+

358 

+

359 def testFirstLineNum(self): 

+

360 infile = """\ 

+

361 fooey 

+

362 [[[cog 

+

363 cog.outl("started at line number %d" % cog.firstLineNum) 

+

364 ]]] 

+

365 started at line number 2 

+

366 [[[end]]] 

+

367 blah blah 

+

368 [[[cog 

+

369 cog.outl("and again at line %d" % cog.firstLineNum) 

+

370 ]]] 

+

371 and again at line 8 

+

372 [[[end]]] 

+

373 """ 

+

374 

+

375 infile = reindentBlock(infile) 

+

376 self.assertEqual(Cog().processString(infile), infile) 

+

377 

+

378 def testCompactOneLineCode(self): 

+

379 infile = """\ 

+

380 first line 

+

381 hey: [[[cog cog.outl("hello %d" % (3*3*3*3)) ]]] looky! 

+

382 get rid of this! 

+

383 [[[end]]] 

+

384 last line 

+

385 """ 

+

386 

+

387 outfile = """\ 

+

388 first line 

+

389 hey: [[[cog cog.outl("hello %d" % (3*3*3*3)) ]]] looky! 

+

390 hello 81 

+

391 [[[end]]] 

+

392 last line 

+

393 """ 

+

394 

+

395 infile = reindentBlock(infile) 

+

396 self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) 

+

397 

+

398 def testInsideOutCompact(self): 

+

399 infile = """\ 

+

400 first line 

+

401 hey?: ]]] what is this? [[[cog strange! 

+

402 get rid of this! 

+

403 [[[end]]] 

+

404 last line 

+

405 """ 

+

406 with self.assertRaisesRegex(CogError, r"^infile.txt\(2\): Cog code markers inverted$"): 

+

407 Cog().processString(reindentBlock(infile), "infile.txt") 

+

408 

+

409 def testSharingGlobals(self): 

+

410 infile = """\ 

+

411 first line 

+

412 hey: [[[cog s="hey there" ]]] looky! 

+

413 [[[end]]] 

+

414 more literal junk. 

+

415 [[[cog cog.outl(s) ]]] 

+

416 [[[end]]] 

+

417 last line 

+

418 """ 

+

419 

+

420 outfile = """\ 

+

421 first line 

+

422 hey: [[[cog s="hey there" ]]] looky! 

+

423 [[[end]]] 

+

424 more literal junk. 

+

425 [[[cog cog.outl(s) ]]] 

+

426 hey there 

+

427 [[[end]]] 

+

428 last line 

+

429 """ 

+

430 

+

431 infile = reindentBlock(infile) 

+

432 self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) 

+

433 

+

434 def testAssertInCogCode(self): 

+

435 # Check that we can test assertions in cog code in the test framework. 

+

436 infile = """\ 

+

437 [[[cog 

+

438 assert 1 == 2, "Oops" 

+

439 ]]] 

+

440 [[[end]]] 

+

441 """ 

+

442 infile = reindentBlock(infile) 

+

443 with self.assertRaisesRegex(CogUserException, "AssertionError: Oops"): 

+

444 Cog().processString(infile) 

+

445 

+

446 def testCogPrevious(self): 

+

447 # Check that we can access the previous run's output. 

+

448 infile = """\ 

+

449 [[[cog 

+

450 assert cog.previous == "Hello there!\\n", "WTF??" 

+

451 cog.out(cog.previous) 

+

452 cog.outl("Ran again!") 

+

453 ]]] 

+

454 Hello there! 

+

455 [[[end]]] 

+

456 """ 

+

457 

+

458 outfile = """\ 

+

459 [[[cog 

+

460 assert cog.previous == "Hello there!\\n", "WTF??" 

+

461 cog.out(cog.previous) 

+

462 cog.outl("Ran again!") 

+

463 ]]] 

+

464 Hello there! 

+

465 Ran again! 

+

466 [[[end]]] 

+

467 """ 

+

468 

+

469 infile = reindentBlock(infile) 

+

470 self.assertEqual(Cog().processString(infile), reindentBlock(outfile)) 

+

471 

+

472 

+

473class CogOptionsTests(TestCase): 

+

474 """ Test the CogOptions class. 

+

475 """ 

+

476 

+

477 def testEquality(self): 

+

478 o = CogOptions() 

+

479 p = CogOptions() 

+

480 self.assertEqual(o, p) 

+

481 o.parseArgs(['-r']) 

+

482 self.assertNotEqual(o, p) 

+

483 p.parseArgs(['-r']) 

+

484 self.assertEqual(o, p) 

+

485 

+

486 def testCloning(self): 

+

487 o = CogOptions() 

+

488 o.parseArgs(['-I', 'fooey', '-I', 'booey', '-s', ' /*x*/']) 

+

489 p = o.clone() 

+

490 self.assertEqual(o, p) 

+

491 p.parseArgs(['-I', 'huey', '-D', 'foo=quux']) 

+

492 self.assertNotEqual(o, p) 

+

493 q = CogOptions() 

+

494 q.parseArgs(['-I', 'fooey', '-I', 'booey', '-s', ' /*x*/', '-I', 'huey', '-D', 'foo=quux']) 

+

495 self.assertEqual(p, q) 

+

496 

+

497 def testCombiningFlags(self): 

+

498 # Single-character flags can be combined. 

+

499 o = CogOptions() 

+

500 o.parseArgs(['-e', '-r', '-z']) 

+

501 p = CogOptions() 

+

502 p.parseArgs(['-erz']) 

+

503 self.assertEqual(o, p) 

+

504 

+

505 def testMarkers(self): 

+

506 o = CogOptions() 

+

507 o._parse_markers('a b c') 

+

508 self.assertEqual('a', o.sBeginSpec) 

+

509 self.assertEqual('b', o.sEndSpec) 

+

510 self.assertEqual('c', o.sEndOutput) 

+

511 

+

512 def testMarkersSwitch(self): 

+

513 o = CogOptions() 

+

514 o.parseArgs(['--markers', 'a b c']) 

+

515 self.assertEqual('a', o.sBeginSpec) 

+

516 self.assertEqual('b', o.sEndSpec) 

+

517 self.assertEqual('c', o.sEndOutput) 

+

518 

+

519 

+

520class FileStructureTests(TestCase): 

+

521 """ Test cases to check that we're properly strict about the structure 

+

522 of files. 

+

523 """ 

+

524 

+

525 def isBad(self, infile, msg=None): 

+

526 infile = reindentBlock(infile) 

+

527 with self.assertRaisesRegex(CogError, "^"+re.escape(msg)+"$"): 

+

528 Cog().processString(infile, 'infile.txt') 

+

529 

+

530 def testBeginNoEnd(self): 

+

531 infile = """\ 

+

532 Fooey 

+

533 #[[[cog 

+

534 cog.outl('hello') 

+

535 """ 

+

536 self.isBad(infile, "infile.txt(2): Cog block begun but never ended.") 

+

537 

+

538 def testNoEoo(self): 

+

539 infile = """\ 

+

540 Fooey 

+

541 #[[[cog 

+

542 cog.outl('hello') 

+

543 #]]] 

+

544 """ 

+

545 self.isBad(infile, "infile.txt(4): Missing '[[[end]]]' before end of file.") 

+

546 

+

547 infile2 = """\ 

+

548 Fooey 

+

549 #[[[cog 

+

550 cog.outl('hello') 

+

551 #]]] 

+

552 #[[[cog 

+

553 cog.outl('goodbye') 

+

554 #]]] 

+

555 """ 

+

556 self.isBad(infile2, "infile.txt(5): Unexpected '[[[cog'") 

+

557 

+

558 def testStartWithEnd(self): 

+

559 infile = """\ 

+

560 #]]] 

+

561 """ 

+

562 self.isBad(infile, "infile.txt(1): Unexpected ']]]'") 

+

563 

+

564 infile2 = """\ 

+

565 #[[[cog 

+

566 cog.outl('hello') 

+

567 #]]] 

+

568 #[[[end]]] 

+

569 #]]] 

+

570 """ 

+

571 self.isBad(infile2, "infile.txt(5): Unexpected ']]]'") 

+

572 

+

573 def testStartWithEoo(self): 

+

574 infile = """\ 

+

575 #[[[end]]] 

+

576 """ 

+

577 self.isBad(infile, "infile.txt(1): Unexpected '[[[end]]]'") 

+

578 

+

579 infile2 = """\ 

+

580 #[[[cog 

+

581 cog.outl('hello') 

+

582 #]]] 

+

583 #[[[end]]] 

+

584 #[[[end]]] 

+

585 """ 

+

586 self.isBad(infile2, "infile.txt(5): Unexpected '[[[end]]]'") 

+

587 

+

588 def testNoEnd(self): 

+

589 infile = """\ 

+

590 #[[[cog 

+

591 cog.outl("hello") 

+

592 #[[[end]]] 

+

593 """ 

+

594 self.isBad(infile, "infile.txt(3): Unexpected '[[[end]]]'") 

+

595 

+

596 infile2 = """\ 

+

597 #[[[cog 

+

598 cog.outl('hello') 

+

599 #]]] 

+

600 #[[[end]]] 

+

601 #[[[cog 

+

602 cog.outl("hello") 

+

603 #[[[end]]] 

+

604 """ 

+

605 self.isBad(infile2, "infile.txt(7): Unexpected '[[[end]]]'") 

+

606 

+

607 def testTwoBegins(self): 

+

608 infile = """\ 

+

609 #[[[cog 

+

610 #[[[cog 

+

611 cog.outl("hello") 

+

612 #]]] 

+

613 #[[[end]]] 

+

614 """ 

+

615 self.isBad(infile, "infile.txt(2): Unexpected '[[[cog'") 

+

616 

+

617 infile2 = """\ 

+

618 #[[[cog 

+

619 cog.outl("hello") 

+

620 #]]] 

+

621 #[[[end]]] 

+

622 #[[[cog 

+

623 #[[[cog 

+

624 cog.outl("hello") 

+

625 #]]] 

+

626 #[[[end]]] 

+

627 """ 

+

628 self.isBad(infile2, "infile.txt(6): Unexpected '[[[cog'") 

+

629 

+

630 def testTwoEnds(self): 

+

631 infile = """\ 

+

632 #[[[cog 

+

633 cog.outl("hello") 

+

634 #]]] 

+

635 #]]] 

+

636 #[[[end]]] 

+

637 """ 

+

638 self.isBad(infile, "infile.txt(4): Unexpected ']]]'") 

+

639 

+

640 infile2 = """\ 

+

641 #[[[cog 

+

642 cog.outl("hello") 

+

643 #]]] 

+

644 #[[[end]]] 

+

645 #[[[cog 

+

646 cog.outl("hello") 

+

647 #]]] 

+

648 #]]] 

+

649 #[[[end]]] 

+

650 """ 

+

651 self.isBad(infile2, "infile.txt(8): Unexpected ']]]'") 

+

652 

+

653 

+

654class CogErrorTests(TestCase): 

+

655 """ Test cases for cog.error(). 

+

656 """ 

+

657 

+

658 def testErrorMsg(self): 

+

659 infile = """\ 

+

660 [[[cog cog.error("This ain't right!")]]] 

+

661 [[[end]]] 

+

662 """ 

+

663 

+

664 infile = reindentBlock(infile) 

+

665 with self.assertRaisesRegex(CogGeneratedError, "^This ain't right!$"): 

+

666 Cog().processString(infile) 

+

667 

+

668 def testErrorNoMsg(self): 

+

669 infile = """\ 

+

670 [[[cog cog.error()]]] 

+

671 [[[end]]] 

+

672 """ 

+

673 

+

674 infile = reindentBlock(infile) 

+

675 with self.assertRaisesRegex(CogGeneratedError, "^Error raised by cog generator.$"): 

+

676 Cog().processString(infile) 

+

677 

+

678 def testNoErrorIfErrorNotCalled(self): 

+

679 infile = """\ 

+

680 --[[[cog 

+

681 --import cog 

+

682 --for i in range(3): 

+

683 -- if i > 10: 

+

684 -- cog.error("Something is amiss!") 

+

685 -- cog.out("xx%d\\n" % i) 

+

686 --]]] 

+

687 xx0 

+

688 xx1 

+

689 xx2 

+

690 --[[[end]]] 

+

691 """ 

+

692 

+

693 infile = reindentBlock(infile) 

+

694 self.assertEqual(Cog().processString(infile), infile) 

+

695 

+

696 

+

697class CogGeneratorGetCodeTests(TestCase): 

+

698 """ Unit tests against CogGenerator to see if its getCode() method works 

+

699 properly. 

+

700 """ 

+

701 

+

702 def setUp(self): 

+

703 """ All tests get a generator to use, and short same-length names for 

+

704 the functions we're going to use. 

+

705 """ 

+

706 self.gen = CogGenerator() 

+

707 self.m = self.gen.parseMarker 

+

708 self.l = self.gen.parseLine 

+

709 

+

710 def testEmpty(self): 

+

711 self.m('// [[[cog') 

+

712 self.m('// ]]]') 

+

713 self.assertEqual(self.gen.getCode(), '') 

+

714 

+

715 def testSimple(self): 

+

716 self.m('// [[[cog') 

+

717 self.l(' print "hello"') 

+

718 self.l(' print "bye"') 

+

719 self.m('// ]]]') 

+

720 self.assertEqual(self.gen.getCode(), 'print "hello"\nprint "bye"') 

+

721 

+

722 def testCompressed1(self): 

+

723 # For a while, I supported compressed code blocks, but no longer. 

+

724 self.m('// [[[cog: print """') 

+

725 self.l('// hello') 

+

726 self.l('// bye') 

+

727 self.m('// """)]]]') 

+

728 self.assertEqual(self.gen.getCode(), 'hello\nbye') 

+

729 

+

730 def testCompressed2(self): 

+

731 # For a while, I supported compressed code blocks, but no longer. 

+

732 self.m('// [[[cog: print """') 

+

733 self.l('hello') 

+

734 self.l('bye') 

+

735 self.m('// """)]]]') 

+

736 self.assertEqual(self.gen.getCode(), 'hello\nbye') 

+

737 

+

738 def testCompressed3(self): 

+

739 # For a while, I supported compressed code blocks, but no longer. 

+

740 self.m('// [[[cog') 

+

741 self.l('print """hello') 

+

742 self.l('bye') 

+

743 self.m('// """)]]]') 

+

744 self.assertEqual(self.gen.getCode(), 'print """hello\nbye') 

+

745 

+

746 def testCompressed4(self): 

+

747 # For a while, I supported compressed code blocks, but no longer. 

+

748 self.m('// [[[cog: print """') 

+

749 self.l('hello') 

+

750 self.l('bye""")') 

+

751 self.m('// ]]]') 

+

752 self.assertEqual(self.gen.getCode(), 'hello\nbye""")') 

+

753 

+

754 def testNoCommonPrefixForMarkers(self): 

+

755 # It's important to be able to use #if 0 to hide lines from a 

+

756 # C++ compiler. 

+

757 self.m('#if 0 //[[[cog') 

+

758 self.l('\timport cog, sys') 

+

759 self.l('') 

+

760 self.l('\tprint sys.argv') 

+

761 self.m('#endif //]]]') 

+

762 self.assertEqual(self.gen.getCode(), 'import cog, sys\n\nprint sys.argv') 

+

763 

+

764 

+

765class TestCaseWithTempDir(TestCase): 

+

766 

+

767 def newCog(self): 

+

768 """ Initialize the cog members for another run. 

+

769 """ 

+

770 # Create a cog engine, and catch its output. 

+

771 self.cog = Cog() 

+

772 self.output = StringIO() 

+

773 self.cog.setOutput(stdout=self.output, stderr=self.output) 

+

774 

+

775 def setUp(self): 

+

776 # Create a temporary directory. 

+

777 self.tempdir = os.path.join(tempfile.gettempdir(), 'testcog_tempdir_' + str(random.random())[2:]) 

+

778 os.mkdir(self.tempdir) 

+

779 self.olddir = os.getcwd() 

+

780 os.chdir(self.tempdir) 

+

781 self.newCog() 

+

782 

+

783 def tearDown(self): 

+

784 os.chdir(self.olddir) 

+

785 # Get rid of the temporary directory. 

+

786 shutil.rmtree(self.tempdir) 

+

787 

+

788 def assertFilesSame(self, sFName1, sFName2): 

+

789 text1 = open(os.path.join(self.tempdir, sFName1), 'rb').read() 

+

790 text2 = open(os.path.join(self.tempdir, sFName2), 'rb').read() 

+

791 self.assertEqual(text1, text2) 

+

792 

+

793 def assertFileContent(self, sFName, sContent): 

+

794 sAbsName = os.path.join(self.tempdir, sFName) 

+

795 f = open(sAbsName, 'rb') 

+

796 try: 

+

797 sFileContent = f.read() 

+

798 finally: 

+

799 f.close() 

+

800 self.assertEqual(sFileContent, to_bytes(sContent)) 

+

801 

+

802 

+

803class ArgumentHandlingTests(TestCaseWithTempDir): 

+

804 

+

805 def testArgumentFailure(self): 

+

806 # Return value 2 means usage problem. 

+

807 self.assertEqual(self.cog.main(['argv0', '-j']), 2) 

+

808 output = self.output.getvalue() 

+

809 self.assertIn("option -j not recognized", output) 

+

810 with self.assertRaisesRegex(CogUsageError, r"^No files to process$"): 

+

811 self.cog.callableMain(['argv0']) 

+

812 with self.assertRaisesRegex(CogUsageError, r"^option -j not recognized$"): 

+

813 self.cog.callableMain(['argv0', '-j']) 

+

814 

+

815 def testNoDashOAndAtFile(self): 

+

816 d = { 

+

817 'cogfiles.txt': """\ 

+

818 # Please run cog 

+

819 """ 

+

820 } 

+

821 

+

822 makeFiles(d) 

+

823 with self.assertRaisesRegex(CogUsageError, r"^Can't use -o with @file$"): 

+

824 self.cog.callableMain(['argv0', '-o', 'foo', '@cogfiles.txt']) 

+

825 

+

826 def testDashV(self): 

+

827 self.assertEqual(self.cog.main(['argv0', '-v']), 0) 

+

828 output = self.output.getvalue() 

+

829 self.assertEqual('Cog version %s\n' % __version__, output) 

+

830 

+

831 def producesHelp(self, args): 

+

832 self.newCog() 

+

833 argv = ['argv0'] + args.split() 

+

834 self.assertEqual(self.cog.main(argv), 0) 

+

835 self.assertEqual(usage, self.output.getvalue()) 

+

836 

+

837 def testDashH(self): 

+

838 # -h or -? anywhere on the command line should just print help. 

+

839 self.producesHelp("-h") 

+

840 self.producesHelp("-?") 

+

841 self.producesHelp("fooey.txt -h") 

+

842 self.producesHelp("-o -r @fooey.txt -? @booey.txt") 

+

843 

+

844 def testDashOAndDashR(self): 

+

845 d = { 

+

846 'cogfile.txt': """\ 

+

847 # Please run cog 

+

848 """ 

+

849 } 

+

850 

+

851 makeFiles(d) 

+

852 with self.assertRaisesRegex(CogUsageError, r"^Can't use -o with -r \(they are opposites\)$"): 

+

853 self.cog.callableMain(['argv0', '-o', 'foo', '-r', 'cogfile.txt']) 

+

854 

+

855 def testDashZ(self): 

+

856 d = { 

+

857 'test.cog': """\ 

+

858 // This is my C++ file. 

+

859 //[[[cog 

+

860 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

861 for fn in fnames: 

+

862 cog.outl("void %s();" % fn) 

+

863 //]]] 

+

864 """, 

+

865 

+

866 'test.out': """\ 

+

867 // This is my C++ file. 

+

868 //[[[cog 

+

869 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

870 for fn in fnames: 

+

871 cog.outl("void %s();" % fn) 

+

872 //]]] 

+

873 void DoSomething(); 

+

874 void DoAnotherThing(); 

+

875 void DoLastThing(); 

+

876 """, 

+

877 } 

+

878 

+

879 makeFiles(d) 

+

880 with self.assertRaisesRegex(CogError, r"^test.cog\(6\): Missing '\[\[\[end\]\]\]' before end of file.$"): 

+

881 self.cog.callableMain(['argv0', '-r', 'test.cog']) 

+

882 self.newCog() 

+

883 self.cog.callableMain(['argv0', '-r', '-z', 'test.cog']) 

+

884 self.assertFilesSame('test.cog', 'test.out') 

+

885 

+

886 def testBadDashD(self): 

+

887 with self.assertRaisesRegex(CogUsageError, r"^-D takes a name=value argument$"): 

+

888 self.cog.callableMain(['argv0', '-Dfooey', 'cog.txt']) 

+

889 with self.assertRaisesRegex(CogUsageError, r"^-D takes a name=value argument$"): 

+

890 self.cog.callableMain(['argv0', '-D', 'fooey', 'cog.txt']) 

+

891 

+

892 def testBadMarkers(self): 

+

893 with self.assertRaisesRegex(CogUsageError, r"^--markers requires 3 values separated by spaces, could not parse 'X'$"): 

+

894 self.cog.callableMain(['argv0', '--markers=X']) 

+

895 with self.assertRaisesRegex(CogUsageError, r"^--markers requires 3 values separated by spaces, could not parse 'A B C D'$"): 

+

896 self.cog.callableMain(['argv0', '--markers=A B C D']) 

+

897 

+

898 

+

899class TestMain(TestCaseWithTempDir): 

+

900 def setUp(self): 

+

901 super(TestMain, self).setUp() 

+

902 self.old_argv = sys.argv[:] 

+

903 self.old_stderr = sys.stderr 

+

904 sys.stderr = StringIO() 

+

905 

+

906 def tearDown(self): 

+

907 sys.stderr = self.old_stderr 

+

908 sys.argv = self.old_argv 

+

909 sys.modules.pop('mycode', None) 

+

910 super(TestMain, self).tearDown() 

+

911 

+

912 def test_main_function(self): 

+

913 sys.argv = ["argv0", "-Z"] 

+

914 ret = main() 

+

915 self.assertEqual(ret, 2) 

+

916 stderr = sys.stderr.getvalue() 

+

917 self.assertEqual(stderr, 'option -Z not recognized\n(for help use -?)\n') 

+

918 

+

919 files = { 

+

920 'test.cog': """\ 

+

921 //[[[cog 

+

922 def func(): 

+

923 import mycode 

+

924 mycode.boom() 

+

925 //]]] 

+

926 //[[[end]]] 

+

927 ----- 

+

928 //[[[cog 

+

929 func() 

+

930 //]]] 

+

931 //[[[end]]] 

+

932 """, 

+

933 

+

934 'mycode.py': """\ 

+

935 def boom(): 

+

936 [][0] 

+

937 """, 

+

938 } 

+

939 

+

940 def test_error_report(self): 

+

941 self.check_error_report() 

+

942 

+

943 def test_error_report_with_prologue(self): 

+

944 self.check_error_report("-p", "#1\n#2") 

+

945 

+

946 def check_error_report(self, *args): 

+

947 """Check that the error report is right.""" 

+

948 makeFiles(self.files) 

+

949 sys.argv = ["argv0"] + list(args) + ["-r", "test.cog"] 

+

950 main() 

+

951 expected = reindentBlock("""\ 

+

952 Traceback (most recent call last): 

+

953 File "test.cog", line 9, in <module> 

+

954 func() 

+

955 File "test.cog", line 4, in func 

+

956 mycode.boom() 

+

957 File "MYCODE", line 2, in boom 

+

958 [][0] 

+

959 IndexError: list index out of range 

+

960 """) 

+

961 if PY3: 

+

962 expected = expected.replace("MYCODE", os.path.abspath("mycode.py")) 

+

963 else: 

+

964 expected = expected.replace("MYCODE", "mycode.py") 

+

965 assert expected == sys.stderr.getvalue() 

+

966 

+

967 def test_error_in_prologue(self): 

+

968 makeFiles(self.files) 

+

969 sys.argv = ["argv0", "-p", "import mycode; mycode.boom()", "-r", "test.cog"] 

+

970 main() 

+

971 expected = reindentBlock("""\ 

+

972 Traceback (most recent call last): 

+

973 File "<prologue>", line 1, in <module> 

+

974 import mycode; mycode.boom() 

+

975 File "MYCODE", line 2, in boom 

+

976 [][0] 

+

977 IndexError: list index out of range 

+

978 """) 

+

979 if PY3: 

+

980 expected = expected.replace("MYCODE", os.path.abspath("mycode.py")) 

+

981 else: 

+

982 expected = expected.replace("MYCODE", "mycode.py") 

+

983 assert expected == sys.stderr.getvalue() 

+

984 

+

985 

+

986 

+

987class TestFileHandling(TestCaseWithTempDir): 

+

988 

+

989 def testSimple(self): 

+

990 d = { 

+

991 'test.cog': """\ 

+

992 // This is my C++ file. 

+

993 //[[[cog 

+

994 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

995 for fn in fnames: 

+

996 cog.outl("void %s();" % fn) 

+

997 //]]] 

+

998 //[[[end]]] 

+

999 """, 

+

1000 

+

1001 'test.out': """\ 

+

1002 // This is my C++ file. 

+

1003 //[[[cog 

+

1004 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1005 for fn in fnames: 

+

1006 cog.outl("void %s();" % fn) 

+

1007 //]]] 

+

1008 void DoSomething(); 

+

1009 void DoAnotherThing(); 

+

1010 void DoLastThing(); 

+

1011 //[[[end]]] 

+

1012 """, 

+

1013 } 

+

1014 

+

1015 makeFiles(d) 

+

1016 self.cog.callableMain(['argv0', '-r', 'test.cog']) 

+

1017 self.assertFilesSame('test.cog', 'test.out') 

+

1018 output = self.output.getvalue() 

+

1019 self.assertIn("(changed)", output) 

+

1020 

+

1021 def testWildcards(self): 

+

1022 d = { 

+

1023 'test.cog': """\ 

+

1024 // This is my C++ file. 

+

1025 //[[[cog 

+

1026 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1027 for fn in fnames: 

+

1028 cog.outl("void %s();" % fn) 

+

1029 //]]] 

+

1030 //[[[end]]] 

+

1031 """, 

+

1032 

+

1033 'test2.cog': """\ 

+

1034 // This is my C++ file. 

+

1035 //[[[cog 

+

1036 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1037 for fn in fnames: 

+

1038 cog.outl("void %s();" % fn) 

+

1039 //]]] 

+

1040 //[[[end]]] 

+

1041 """, 

+

1042 

+

1043 'test.out': """\ 

+

1044 // This is my C++ file. 

+

1045 //[[[cog 

+

1046 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1047 for fn in fnames: 

+

1048 cog.outl("void %s();" % fn) 

+

1049 //]]] 

+

1050 void DoSomething(); 

+

1051 void DoAnotherThing(); 

+

1052 void DoLastThing(); 

+

1053 //[[[end]]] 

+

1054 """, 

+

1055 

+

1056 'not_this_one.cog': """\ 

+

1057 // This is my C++ file. 

+

1058 //[[[cog 

+

1059 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1060 for fn in fnames: 

+

1061 cog.outl("void %s();" % fn) 

+

1062 //]]] 

+

1063 //[[[end]]] 

+

1064 """, 

+

1065 

+

1066 'not_this_one.out': """\ 

+

1067 // This is my C++ file. 

+

1068 //[[[cog 

+

1069 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1070 for fn in fnames: 

+

1071 cog.outl("void %s();" % fn) 

+

1072 //]]] 

+

1073 //[[[end]]] 

+

1074 """, 

+

1075 } 

+

1076 

+

1077 makeFiles(d) 

+

1078 self.cog.callableMain(['argv0', '-r', 't*.cog']) 

+

1079 self.assertFilesSame('test.cog', 'test.out') 

+

1080 self.assertFilesSame('test2.cog', 'test.out') 

+

1081 self.assertFilesSame('not_this_one.cog', 'not_this_one.out') 

+

1082 output = self.output.getvalue() 

+

1083 self.assertIn("(changed)", output) 

+

1084 

+

1085 def testOutputFile(self): 

+

1086 # -o sets the output file. 

+

1087 d = { 

+

1088 'test.cog': """\ 

+

1089 // This is my C++ file. 

+

1090 //[[[cog 

+

1091 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1092 for fn in fnames: 

+

1093 cog.outl("void %s();" % fn) 

+

1094 //]]] 

+

1095 //[[[end]]] 

+

1096 """, 

+

1097 

+

1098 'test.out': """\ 

+

1099 // This is my C++ file. 

+

1100 //[[[cog 

+

1101 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

1102 for fn in fnames: 

+

1103 cog.outl("void %s();" % fn) 

+

1104 //]]] 

+

1105 void DoSomething(); 

+

1106 void DoAnotherThing(); 

+

1107 void DoLastThing(); 

+

1108 //[[[end]]] 

+

1109 """, 

+

1110 } 

+

1111 

+

1112 makeFiles(d) 

+

1113 self.cog.callableMain(['argv0', '-o', 'in/a/dir/test.cogged', 'test.cog']) 

+

1114 self.assertFilesSame('in/a/dir/test.cogged', 'test.out') 

+

1115 

+

1116 def testAtFile(self): 

+

1117 d = { 

+

1118 'one.cog': """\ 

+

1119 //[[[cog 

+

1120 cog.outl("hello world") 

+

1121 //]]] 

+

1122 //[[[end]]] 

+

1123 """, 

+

1124 

+

1125 'one.out': """\ 

+

1126 //[[[cog 

+

1127 cog.outl("hello world") 

+

1128 //]]] 

+

1129 hello world 

+

1130 //[[[end]]] 

+

1131 """, 

+

1132 

+

1133 'two.cog': """\ 

+

1134 //[[[cog 

+

1135 cog.outl("goodbye cruel world") 

+

1136 //]]] 

+

1137 //[[[end]]] 

+

1138 """, 

+

1139 

+

1140 'two.out': """\ 

+

1141 //[[[cog 

+

1142 cog.outl("goodbye cruel world") 

+

1143 //]]] 

+

1144 goodbye cruel world 

+

1145 //[[[end]]] 

+

1146 """, 

+

1147 

+

1148 'cogfiles.txt': """\ 

+

1149 # Please run cog 

+

1150 one.cog 

+

1151 

+

1152 two.cog 

+

1153 """ 

+

1154 } 

+

1155 

+

1156 makeFiles(d) 

+

1157 self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

+

1158 self.assertFilesSame('one.cog', 'one.out') 

+

1159 self.assertFilesSame('two.cog', 'two.out') 

+

1160 output = self.output.getvalue() 

+

1161 self.assertIn("(changed)", output) 

+

1162 

+

1163 def testNestedAtFile(self): 

+

1164 d = { 

+

1165 'one.cog': """\ 

+

1166 //[[[cog 

+

1167 cog.outl("hello world") 

+

1168 //]]] 

+

1169 //[[[end]]] 

+

1170 """, 

+

1171 

+

1172 'one.out': """\ 

+

1173 //[[[cog 

+

1174 cog.outl("hello world") 

+

1175 //]]] 

+

1176 hello world 

+

1177 //[[[end]]] 

+

1178 """, 

+

1179 

+

1180 'two.cog': """\ 

+

1181 //[[[cog 

+

1182 cog.outl("goodbye cruel world") 

+

1183 //]]] 

+

1184 //[[[end]]] 

+

1185 """, 

+

1186 

+

1187 'two.out': """\ 

+

1188 //[[[cog 

+

1189 cog.outl("goodbye cruel world") 

+

1190 //]]] 

+

1191 goodbye cruel world 

+

1192 //[[[end]]] 

+

1193 """, 

+

1194 

+

1195 'cogfiles.txt': """\ 

+

1196 # Please run cog 

+

1197 one.cog 

+

1198 @cogfiles2.txt 

+

1199 """, 

+

1200 

+

1201 'cogfiles2.txt': """\ 

+

1202 # This one too, please. 

+

1203 two.cog 

+

1204 """, 

+

1205 } 

+

1206 

+

1207 makeFiles(d) 

+

1208 self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

+

1209 self.assertFilesSame('one.cog', 'one.out') 

+

1210 self.assertFilesSame('two.cog', 'two.out') 

+

1211 output = self.output.getvalue() 

+

1212 self.assertIn("(changed)", output) 

+

1213 

+

1214 def testAtFileWithArgs(self): 

+

1215 d = { 

+

1216 'both.cog': """\ 

+

1217 //[[[cog 

+

1218 cog.outl("one: %s" % ('one' in globals())) 

+

1219 cog.outl("two: %s" % ('two' in globals())) 

+

1220 //]]] 

+

1221 //[[[end]]] 

+

1222 """, 

+

1223 

+

1224 'one.out': """\ 

+

1225 //[[[cog 

+

1226 cog.outl("one: %s" % ('one' in globals())) 

+

1227 cog.outl("two: %s" % ('two' in globals())) 

+

1228 //]]] 

+

1229 one: True // ONE 

+

1230 two: False // ONE 

+

1231 //[[[end]]] 

+

1232 """, 

+

1233 

+

1234 'two.out': """\ 

+

1235 //[[[cog 

+

1236 cog.outl("one: %s" % ('one' in globals())) 

+

1237 cog.outl("two: %s" % ('two' in globals())) 

+

1238 //]]] 

+

1239 one: False // TWO 

+

1240 two: True // TWO 

+

1241 //[[[end]]] 

+

1242 """, 

+

1243 

+

1244 'cogfiles.txt': """\ 

+

1245 # Please run cog 

+

1246 both.cog -o in/a/dir/both.one -s ' // ONE' -D one=x 

+

1247 both.cog -o in/a/dir/both.two -s ' // TWO' -D two=x 

+

1248 """ 

+

1249 } 

+

1250 

+

1251 makeFiles(d) 

+

1252 self.cog.callableMain(['argv0', '@cogfiles.txt']) 

+

1253 self.assertFilesSame('in/a/dir/both.one', 'one.out') 

+

1254 self.assertFilesSame('in/a/dir/both.two', 'two.out') 

+

1255 

+

1256 def testAtFileWithBadArgCombo(self): 

+

1257 d = { 

+

1258 'both.cog': """\ 

+

1259 //[[[cog 

+

1260 cog.outl("one: %s" % ('one' in globals())) 

+

1261 cog.outl("two: %s" % ('two' in globals())) 

+

1262 //]]] 

+

1263 //[[[end]]] 

+

1264 """, 

+

1265 

+

1266 'cogfiles.txt': """\ 

+

1267 # Please run cog 

+

1268 both.cog 

+

1269 both.cog -d # This is bad: -r and -d 

+

1270 """ 

+

1271 } 

+

1272 

+

1273 makeFiles(d) 

+

1274 with self.assertRaisesRegex(CogUsageError, r"^Can't use -d with -r \(or you would delete all your source!\)$"): 

+

1275 self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

+

1276 

+

1277 def testAtFileWithTrickyFilenames(self): 

+

1278 def fix_backslashes(files_txt): 

+

1279 """Make the contents of a files.txt sensitive to the platform.""" 

+

1280 if sys.platform != "win32": 

+

1281 files_txt = files_txt.replace("\\", "/") 

+

1282 return files_txt 

+

1283 

+

1284 d = { 

+

1285 'one 1.cog': """\ 

+

1286 //[[[cog cog.outl("hello world") ]]] 

+

1287 """, 

+

1288 

+

1289 'one.out': """\ 

+

1290 //[[[cog cog.outl("hello world") ]]] 

+

1291 hello world //xxx 

+

1292 """, 

+

1293 

+

1294 'subdir': { 

+

1295 'subback.cog': """\ 

+

1296 //[[[cog cog.outl("down deep with backslashes") ]]] 

+

1297 """, 

+

1298 

+

1299 'subfwd.cog': """\ 

+

1300 //[[[cog cog.outl("down deep with slashes") ]]] 

+

1301 """, 

+

1302 }, 

+

1303 

+

1304 'subback.out': """\ 

+

1305 //[[[cog cog.outl("down deep with backslashes") ]]] 

+

1306 down deep with backslashes //yyy 

+

1307 """, 

+

1308 

+

1309 'subfwd.out': """\ 

+

1310 //[[[cog cog.outl("down deep with slashes") ]]] 

+

1311 down deep with slashes //zzz 

+

1312 """, 

+

1313 

+

1314 'cogfiles.txt': fix_backslashes("""\ 

+

1315 # Please run cog 

+

1316 'one 1.cog' -s ' //xxx' 

+

1317 subdir\\subback.cog -s ' //yyy' 

+

1318 subdir/subfwd.cog -s ' //zzz' 

+

1319 """) 

+

1320 } 

+

1321 

+

1322 makeFiles(d) 

+

1323 self.cog.callableMain(['argv0', '-z', '-r', '@cogfiles.txt']) 

+

1324 self.assertFilesSame('one 1.cog', 'one.out') 

+

1325 self.assertFilesSame('subdir/subback.cog', 'subback.out') 

+

1326 self.assertFilesSame('subdir/subfwd.cog', 'subfwd.out') 

+

1327 

+

1328 def run_with_verbosity(self, verbosity): 

+

1329 d = { 

+

1330 'unchanged.cog': """\ 

+

1331 //[[[cog 

+

1332 cog.outl("hello world") 

+

1333 //]]] 

+

1334 hello world 

+

1335 //[[[end]]] 

+

1336 """, 

+

1337 

+

1338 'changed.cog': """\ 

+

1339 //[[[cog 

+

1340 cog.outl("goodbye cruel world") 

+

1341 //]]] 

+

1342 //[[[end]]] 

+

1343 """, 

+

1344 

+

1345 'cogfiles.txt': """\ 

+

1346 unchanged.cog 

+

1347 changed.cog 

+

1348 """ 

+

1349 } 

+

1350 

+

1351 makeFiles(d) 

+

1352 self.cog.callableMain(['argv0', '-r', '--verbosity='+verbosity, '@cogfiles.txt']) 

+

1353 output = self.output.getvalue() 

+

1354 return output 

+

1355 

+

1356 def test_verbosity0(self): 

+

1357 output = self.run_with_verbosity("0") 

+

1358 self.assertEqual(output, "") 

+

1359 

+

1360 def test_verbosity1(self): 

+

1361 output = self.run_with_verbosity("1") 

+

1362 self.assertEqual(output, "Cogging changed.cog (changed)\n") 

+

1363 

+

1364 def test_verbosity2(self): 

+

1365 output = self.run_with_verbosity("2") 

+

1366 self.assertEqual(output, "Cogging unchanged.cog\nCogging changed.cog (changed)\n") 

+

1367 

+

1368 

+

1369class CogTestLineEndings(TestCaseWithTempDir): 

+

1370 """Tests for -U option (force LF line-endings in output).""" 

+

1371 

+

1372 lines_in = ['Some text.', 

+

1373 '//[[[cog', 

+

1374 'cog.outl("Cog text")', 

+

1375 '//]]]', 

+

1376 'gobbledegook.', 

+

1377 '//[[[end]]]', 

+

1378 'epilogue.', 

+

1379 ''] 

+

1380 

+

1381 lines_out = ['Some text.', 

+

1382 '//[[[cog', 

+

1383 'cog.outl("Cog text")', 

+

1384 '//]]]', 

+

1385 'Cog text', 

+

1386 '//[[[end]]]', 

+

1387 'epilogue.', 

+

1388 ''] 

+

1389 

+

1390 def testOutputNativeEol(self): 

+

1391 makeFiles({'infile': '\n'.join(self.lines_in)}) 

+

1392 self.cog.callableMain(['argv0', '-o', 'outfile', 'infile']) 

+

1393 self.assertFileContent('outfile', os.linesep.join(self.lines_out)) 

+

1394 

+

1395 def testOutputLfEol(self): 

+

1396 makeFiles({'infile': '\n'.join(self.lines_in)}) 

+

1397 self.cog.callableMain(['argv0', '-U', '-o', 'outfile', 'infile']) 

+

1398 self.assertFileContent('outfile', '\n'.join(self.lines_out)) 

+

1399 

+

1400 def testReplaceNativeEol(self): 

+

1401 makeFiles({'test.cog': '\n'.join(self.lines_in)}) 

+

1402 self.cog.callableMain(['argv0', '-r', 'test.cog']) 

+

1403 self.assertFileContent('test.cog', os.linesep.join(self.lines_out)) 

+

1404 

+

1405 def testReplaceLfEol(self): 

+

1406 makeFiles({'test.cog': '\n'.join(self.lines_in)}) 

+

1407 self.cog.callableMain(['argv0', '-U', '-r', 'test.cog']) 

+

1408 self.assertFileContent('test.cog', '\n'.join(self.lines_out)) 

+

1409 

+

1410 

+

1411class CogTestCharacterEncoding(TestCaseWithTempDir): 

+

1412 

+

1413 def testSimple(self): 

+

1414 d = { 

+

1415 'test.cog': b"""\ 

+

1416 // This is my C++ file. 

+

1417 //[[[cog 

+

1418 cog.outl("// Unicode: \xe1\x88\xb4 (U+1234)") 

+

1419 //]]] 

+

1420 //[[[end]]] 

+

1421 """, 

+

1422 

+

1423 'test.out': b"""\ 

+

1424 // This is my C++ file. 

+

1425 //[[[cog 

+

1426 cog.outl("// Unicode: \xe1\x88\xb4 (U+1234)") 

+

1427 //]]] 

+

1428 // Unicode: \xe1\x88\xb4 (U+1234) 

+

1429 //[[[end]]] 

+

1430 """.replace(b"\n", os.linesep.encode()), 

+

1431 } 

+

1432 

+

1433 makeFiles(d, bytes=True) 

+

1434 self.cog.callableMain(['argv0', '-r', 'test.cog']) 

+

1435 self.assertFilesSame('test.cog', 'test.out') 

+

1436 output = self.output.getvalue() 

+

1437 self.assertIn("(changed)", output) 

+

1438 

+

1439 def testFileEncodingOption(self): 

+

1440 d = { 

+

1441 'test.cog': b"""\ 

+

1442 // \xca\xee\xe4\xe8\xf0\xe2\xea\xe0 Windows 

+

1443 //[[[cog 

+

1444 cog.outl("\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe") 

+

1445 //]]] 

+

1446 //[[[end]]] 

+

1447 """, 

+

1448 

+

1449 'test.out': b"""\ 

+

1450 // \xca\xee\xe4\xe8\xf0\xe2\xea\xe0 Windows 

+

1451 //[[[cog 

+

1452 cog.outl("\xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe") 

+

1453 //]]] 

+

1454 \xd1\xfa\xe5\xf8\xfc \xe5\xf9\xb8 \xfd\xf2\xe8\xf5 \xec\xff\xe3\xea\xe8\xf5 \xf4\xf0\xe0\xed\xf6\xf3\xe7\xf1\xea\xe8\xf5 \xe1\xf3\xeb\xee\xea \xe4\xe0 \xe2\xfb\xef\xe5\xe9 \xf7\xe0\xfe 

+

1455 //[[[end]]] 

+

1456 """.replace(b"\n", os.linesep.encode()), 

+

1457 } 

+

1458 

+

1459 makeFiles(d, bytes=True) 

+

1460 self.cog.callableMain(['argv0', '-n', 'cp1251', '-r', 'test.cog']) 

+

1461 self.assertFilesSame('test.cog', 'test.out') 

+

1462 output = self.output.getvalue() 

+

1463 self.assertIn("(changed)", output) 

+

1464 

+

1465 

+

1466class TestCaseWithImports(TestCaseWithTempDir): 

+

1467 """ When running tests which import modules, the sys.modules list 

+

1468 leaks from one test to the next. This test case class scrubs 

+

1469 the list after each run to keep the tests isolated from each other. 

+

1470 """ 

+

1471 

+

1472 def setUp(self): 

+

1473 super(TestCaseWithImports, self).setUp() 

+

1474 self.sysmodulekeys = list(sys.modules) 

+

1475 

+

1476 def tearDown(self): 

+

1477 modstoscrub = [ 

+

1478 modname 

+

1479 for modname in sys.modules 

+

1480 if modname not in self.sysmodulekeys 

+

1481 ] 

+

1482 for modname in modstoscrub: 

+

1483 del sys.modules[modname] 

+

1484 super(TestCaseWithImports, self).tearDown() 

+

1485 

+

1486 

+

1487class CogIncludeTests(TestCaseWithImports): 

+

1488 dincludes = { 

+

1489 'test.cog': """\ 

+

1490 //[[[cog 

+

1491 import mymodule 

+

1492 //]]] 

+

1493 //[[[end]]] 

+

1494 """, 

+

1495 

+

1496 'test.out': """\ 

+

1497 //[[[cog 

+

1498 import mymodule 

+

1499 //]]] 

+

1500 Hello from mymodule 

+

1501 //[[[end]]] 

+

1502 """, 

+

1503 

+

1504 'test2.out': """\ 

+

1505 //[[[cog 

+

1506 import mymodule 

+

1507 //]]] 

+

1508 Hello from mymodule in inc2 

+

1509 //[[[end]]] 

+

1510 """, 

+

1511 

+

1512 'include': { 

+

1513 'mymodule.py': """\ 

+

1514 import cog 

+

1515 cog.outl("Hello from mymodule") 

+

1516 """ 

+

1517 }, 

+

1518 

+

1519 'inc2': { 

+

1520 'mymodule.py': """\ 

+

1521 import cog 

+

1522 cog.outl("Hello from mymodule in inc2") 

+

1523 """ 

+

1524 }, 

+

1525 

+

1526 'inc3': { 

+

1527 'someothermodule.py': """\ 

+

1528 import cog 

+

1529 cog.outl("This is some other module.") 

+

1530 """ 

+

1531 }, 

+

1532 } 

+

1533 

+

1534 def testNeedIncludePath(self): 

+

1535 # Try it without the -I, to see that an ImportError happens. 

+

1536 makeFiles(self.dincludes) 

+

1537 msg = "(ImportError|ModuleNotFoundError): No module named '?mymodule'?" 

+

1538 with self.assertRaisesRegex(CogUserException, msg): 

+

1539 self.cog.callableMain(['argv0', '-r', 'test.cog']) 

+

1540 

+

1541 def testIncludePath(self): 

+

1542 # Test that -I adds include directories properly. 

+

1543 makeFiles(self.dincludes) 

+

1544 self.cog.callableMain(['argv0', '-r', '-I', 'include', 'test.cog']) 

+

1545 self.assertFilesSame('test.cog', 'test.out') 

+

1546 

+

1547 def testTwoIncludePaths(self): 

+

1548 # Test that two -I's add include directories properly. 

+

1549 makeFiles(self.dincludes) 

+

1550 self.cog.callableMain(['argv0', '-r', '-I', 'include', '-I', 'inc2', 'test.cog']) 

+

1551 self.assertFilesSame('test.cog', 'test.out') 

+

1552 

+

1553 def testTwoIncludePaths2(self): 

+

1554 # Test that two -I's add include directories properly. 

+

1555 makeFiles(self.dincludes) 

+

1556 self.cog.callableMain(['argv0', '-r', '-I', 'inc2', '-I', 'include', 'test.cog']) 

+

1557 self.assertFilesSame('test.cog', 'test2.out') 

+

1558 

+

1559 def testUselessIncludePath(self): 

+

1560 # Test that the search will continue past the first directory. 

+

1561 makeFiles(self.dincludes) 

+

1562 self.cog.callableMain(['argv0', '-r', '-I', 'inc3', '-I', 'include', 'test.cog']) 

+

1563 self.assertFilesSame('test.cog', 'test.out') 

+

1564 

+

1565 def testSysPathIsUnchanged(self): 

+

1566 d = { 

+

1567 'bad.cog': """\ 

+

1568 //[[[cog cog.error("Oh no!") ]]] 

+

1569 //[[[end]]] 

+

1570 """, 

+

1571 'good.cog': """\ 

+

1572 //[[[cog cog.outl("Oh yes!") ]]] 

+

1573 //[[[end]]] 

+

1574 """, 

+

1575 } 

+

1576 

+

1577 makeFiles(d) 

+

1578 # Is it unchanged just by creating a cog engine? 

+

1579 oldsyspath = sys.path[:] 

+

1580 self.newCog() 

+

1581 self.assertEqual(oldsyspath, sys.path) 

+

1582 # Is it unchanged for a successful run? 

+

1583 self.newCog() 

+

1584 self.cog.callableMain(['argv0', '-r', 'good.cog']) 

+

1585 self.assertEqual(oldsyspath, sys.path) 

+

1586 # Is it unchanged for a successful run with includes? 

+

1587 self.newCog() 

+

1588 self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', 'good.cog']) 

+

1589 self.assertEqual(oldsyspath, sys.path) 

+

1590 # Is it unchanged for a successful run with two includes? 

+

1591 self.newCog() 

+

1592 self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', '-I', 'quux', 'good.cog']) 

+

1593 self.assertEqual(oldsyspath, sys.path) 

+

1594 # Is it unchanged for a failed run? 

+

1595 self.newCog() 

+

1596 with self.assertRaisesRegex(CogError, r"^Oh no!$"): 

+

1597 self.cog.callableMain(['argv0', '-r', 'bad.cog']) 

+

1598 self.assertEqual(oldsyspath, sys.path) 

+

1599 # Is it unchanged for a failed run with includes? 

+

1600 self.newCog() 

+

1601 with self.assertRaisesRegex(CogError, r"^Oh no!$"): 

+

1602 self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', 'bad.cog']) 

+

1603 self.assertEqual(oldsyspath, sys.path) 

+

1604 # Is it unchanged for a failed run with two includes? 

+

1605 self.newCog() 

+

1606 with self.assertRaisesRegex(CogError, r"^Oh no!$"): 

+

1607 self.cog.callableMain(['argv0', '-r', '-I', 'xyzzy', '-I', 'quux', 'bad.cog']) 

+

1608 self.assertEqual(oldsyspath, sys.path) 

+

1609 

+

1610 def testSubDirectories(self): 

+

1611 # Test that relative paths on the command line work, with includes. 

+

1612 

+

1613 d = { 

+

1614 'code': { 

+

1615 'test.cog': """\ 

+

1616 //[[[cog 

+

1617 import mysubmodule 

+

1618 //]]] 

+

1619 //[[[end]]] 

+

1620 """, 

+

1621 

+

1622 'test.out': """\ 

+

1623 //[[[cog 

+

1624 import mysubmodule 

+

1625 //]]] 

+

1626 Hello from mysubmodule 

+

1627 //[[[end]]] 

+

1628 """, 

+

1629 

+

1630 'mysubmodule.py': """\ 

+

1631 import cog 

+

1632 cog.outl("Hello from mysubmodule") 

+

1633 """ 

+

1634 } 

+

1635 } 

+

1636 

+

1637 makeFiles(d) 

+

1638 # We should be able to invoke cog without the -I switch, and it will 

+

1639 # auto-include the current directory 

+

1640 self.cog.callableMain(['argv0', '-r', 'code/test.cog']) 

+

1641 self.assertFilesSame('code/test.cog', 'code/test.out') 

+

1642 

+

1643 

+

1644class CogTestsInFiles(TestCaseWithTempDir): 

+

1645 

+

1646 def testWarnIfNoCogCode(self): 

+

1647 # Test that the -e switch warns if there is no Cog code. 

+

1648 d = { 

+

1649 'with.cog': """\ 

+

1650 //[[[cog 

+

1651 cog.outl("hello world") 

+

1652 //]]] 

+

1653 hello world 

+

1654 //[[[end]]] 

+

1655 """, 

+

1656 

+

1657 'without.cog': """\ 

+

1658 There's no cog 

+

1659 code in this file. 

+

1660 """, 

+

1661 } 

+

1662 

+

1663 makeFiles(d) 

+

1664 self.cog.callableMain(['argv0', '-e', 'with.cog']) 

+

1665 output = self.output.getvalue() 

+

1666 self.assertNotIn("Warning", output) 

+

1667 self.newCog() 

+

1668 self.cog.callableMain(['argv0', '-e', 'without.cog']) 

+

1669 output = self.output.getvalue() 

+

1670 self.assertIn("Warning: no cog code found in without.cog", output) 

+

1671 self.newCog() 

+

1672 self.cog.callableMain(['argv0', 'without.cog']) 

+

1673 output = self.output.getvalue() 

+

1674 self.assertNotIn("Warning", output) 

+

1675 

+

1676 def testFileNameProps(self): 

+

1677 d = { 

+

1678 'cog1.txt': """\ 

+

1679 //[[[cog 

+

1680 cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) 

+

1681 //]]] 

+

1682 this is cog1.txt in, cog1.txt out 

+

1683 [[[end]]] 

+

1684 """, 

+

1685 

+

1686 'cog1.out': """\ 

+

1687 //[[[cog 

+

1688 cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) 

+

1689 //]]] 

+

1690 This is cog1.txt in, cog1.txt out 

+

1691 [[[end]]] 

+

1692 """, 

+

1693 

+

1694 'cog1out.out': """\ 

+

1695 //[[[cog 

+

1696 cog.outl("This is %s in, %s out" % (cog.inFile, cog.outFile)) 

+

1697 //]]] 

+

1698 This is cog1.txt in, cog1out.txt out 

+

1699 [[[end]]] 

+

1700 """, 

+

1701 } 

+

1702 

+

1703 makeFiles(d) 

+

1704 self.cog.callableMain(['argv0', '-r', 'cog1.txt']) 

+

1705 self.assertFilesSame('cog1.txt', 'cog1.out') 

+

1706 self.newCog() 

+

1707 self.cog.callableMain(['argv0', '-o', 'cog1out.txt', 'cog1.txt']) 

+

1708 self.assertFilesSame('cog1out.txt', 'cog1out.out') 

+

1709 

+

1710 def testGlobalsDontCrossFiles(self): 

+

1711 # Make sure that global values don't get shared between files. 

+

1712 d = { 

+

1713 'one.cog': """\ 

+

1714 //[[[cog s = "This was set in one.cog" ]]] 

+

1715 //[[[end]]] 

+

1716 //[[[cog cog.outl(s) ]]] 

+

1717 //[[[end]]] 

+

1718 """, 

+

1719 

+

1720 'one.out': """\ 

+

1721 //[[[cog s = "This was set in one.cog" ]]] 

+

1722 //[[[end]]] 

+

1723 //[[[cog cog.outl(s) ]]] 

+

1724 This was set in one.cog 

+

1725 //[[[end]]] 

+

1726 """, 

+

1727 

+

1728 'two.cog': """\ 

+

1729 //[[[cog 

+

1730 try: 

+

1731 cog.outl(s) 

+

1732 except NameError: 

+

1733 cog.outl("s isn't set!") 

+

1734 //]]] 

+

1735 //[[[end]]] 

+

1736 """, 

+

1737 

+

1738 'two.out': """\ 

+

1739 //[[[cog 

+

1740 try: 

+

1741 cog.outl(s) 

+

1742 except NameError: 

+

1743 cog.outl("s isn't set!") 

+

1744 //]]] 

+

1745 s isn't set! 

+

1746 //[[[end]]] 

+

1747 """, 

+

1748 

+

1749 'cogfiles.txt': """\ 

+

1750 # Please run cog 

+

1751 one.cog 

+

1752 

+

1753 two.cog 

+

1754 """ 

+

1755 } 

+

1756 

+

1757 makeFiles(d) 

+

1758 self.cog.callableMain(['argv0', '-r', '@cogfiles.txt']) 

+

1759 self.assertFilesSame('one.cog', 'one.out') 

+

1760 self.assertFilesSame('two.cog', 'two.out') 

+

1761 output = self.output.getvalue() 

+

1762 self.assertIn("(changed)", output) 

+

1763 

+

1764 def testRemoveGeneratedOutput(self): 

+

1765 d = { 

+

1766 'cog1.txt': """\ 

+

1767 //[[[cog 

+

1768 cog.outl("This line was generated.") 

+

1769 //]]] 

+

1770 This line was generated. 

+

1771 //[[[end]]] 

+

1772 This line was not. 

+

1773 """, 

+

1774 

+

1775 'cog1.out': """\ 

+

1776 //[[[cog 

+

1777 cog.outl("This line was generated.") 

+

1778 //]]] 

+

1779 //[[[end]]] 

+

1780 This line was not. 

+

1781 """, 

+

1782 

+

1783 'cog1.out2': """\ 

+

1784 //[[[cog 

+

1785 cog.outl("This line was generated.") 

+

1786 //]]] 

+

1787 This line was generated. 

+

1788 //[[[end]]] 

+

1789 This line was not. 

+

1790 """, 

+

1791 } 

+

1792 

+

1793 makeFiles(d) 

+

1794 # Remove generated output. 

+

1795 self.cog.callableMain(['argv0', '-r', '-x', 'cog1.txt']) 

+

1796 self.assertFilesSame('cog1.txt', 'cog1.out') 

+

1797 self.newCog() 

+

1798 # Regenerate the generated output. 

+

1799 self.cog.callableMain(['argv0', '-r', 'cog1.txt']) 

+

1800 self.assertFilesSame('cog1.txt', 'cog1.out2') 

+

1801 self.newCog() 

+

1802 # Remove the generated output again. 

+

1803 self.cog.callableMain(['argv0', '-r', '-x', 'cog1.txt']) 

+

1804 self.assertFilesSame('cog1.txt', 'cog1.out') 

+

1805 

+

1806 def testMsgCall(self): 

+

1807 infile = """\ 

+

1808 #[[[cog 

+

1809 cog.msg("Hello there!") 

+

1810 #]]] 

+

1811 #[[[end]]] 

+

1812 """ 

+

1813 infile = reindentBlock(infile) 

+

1814 self.assertEqual(self.cog.processString(infile), infile) 

+

1815 output = self.output.getvalue() 

+

1816 self.assertEqual(output, "Message: Hello there!\n") 

+

1817 

+

1818 def testErrorMessageHasNoTraceback(self): 

+

1819 # Test that a Cog error is printed to stderr with no traceback. 

+

1820 

+

1821 d = { 

+

1822 'cog1.txt': """\ 

+

1823 //[[[cog 

+

1824 cog.outl("This line was newly") 

+

1825 cog.outl("generated by cog") 

+

1826 cog.outl("blah blah.") 

+

1827 //]]] 

+

1828 Xhis line was newly 

+

1829 generated by cog 

+

1830 blah blah. 

+

1831 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

1832 """, 

+

1833 } 

+

1834 

+

1835 makeFiles(d) 

+

1836 stderr = StringIO() 

+

1837 self.cog.setOutput(stderr=stderr) 

+

1838 self.cog.main(['argv0', '-c', '-r', "cog1.txt"]) 

+

1839 self.assertEqual(self.output.getvalue(), "Cogging cog1.txt\n") 

+

1840 self.assertEqual(stderr.getvalue(), "cog1.txt(9): Output has been edited! Delete old checksum to unprotect.\n") 

+

1841 

+

1842 def testDashD(self): 

+

1843 d = { 

+

1844 'test.cog': """\ 

+

1845 --[[[cog cog.outl("Defined fooey as " + fooey) ]]] 

+

1846 --[[[end]]] 

+

1847 """, 

+

1848 

+

1849 'test.kablooey': """\ 

+

1850 --[[[cog cog.outl("Defined fooey as " + fooey) ]]] 

+

1851 Defined fooey as kablooey 

+

1852 --[[[end]]] 

+

1853 """, 

+

1854 

+

1855 'test.einstein': """\ 

+

1856 --[[[cog cog.outl("Defined fooey as " + fooey) ]]] 

+

1857 Defined fooey as e=mc2 

+

1858 --[[[end]]] 

+

1859 """, 

+

1860 } 

+

1861 

+

1862 makeFiles(d) 

+

1863 self.cog.callableMain(['argv0', '-r', '-D', 'fooey=kablooey', 'test.cog']) 

+

1864 self.assertFilesSame('test.cog', 'test.kablooey') 

+

1865 makeFiles(d) 

+

1866 self.cog.callableMain(['argv0', '-r', '-Dfooey=kablooey', 'test.cog']) 

+

1867 self.assertFilesSame('test.cog', 'test.kablooey') 

+

1868 makeFiles(d) 

+

1869 self.cog.callableMain(['argv0', '-r', '-Dfooey=e=mc2', 'test.cog']) 

+

1870 self.assertFilesSame('test.cog', 'test.einstein') 

+

1871 makeFiles(d) 

+

1872 self.cog.callableMain(['argv0', '-r', '-Dbar=quux', '-Dfooey=kablooey', 'test.cog']) 

+

1873 self.assertFilesSame('test.cog', 'test.kablooey') 

+

1874 makeFiles(d) 

+

1875 self.cog.callableMain(['argv0', '-r', '-Dfooey=kablooey', '-Dbar=quux', 'test.cog']) 

+

1876 self.assertFilesSame('test.cog', 'test.kablooey') 

+

1877 makeFiles(d) 

+

1878 self.cog.callableMain(['argv0', '-r', '-Dfooey=gooey', '-Dfooey=kablooey', 'test.cog']) 

+

1879 self.assertFilesSame('test.cog', 'test.kablooey') 

+

1880 

+

1881 def testOutputToStdout(self): 

+

1882 d = { 

+

1883 'test.cog': """\ 

+

1884 --[[[cog cog.outl('Hey there!') ]]] 

+

1885 --[[[end]]] 

+

1886 """ 

+

1887 } 

+

1888 

+

1889 makeFiles(d) 

+

1890 stderr = StringIO() 

+

1891 self.cog.setOutput(stderr=stderr) 

+

1892 self.cog.callableMain(['argv0', 'test.cog']) 

+

1893 output = self.output.getvalue() 

+

1894 outerr = stderr.getvalue() 

+

1895 self.assertEqual(output, "--[[[cog cog.outl('Hey there!') ]]]\nHey there!\n--[[[end]]]\n") 

+

1896 self.assertEqual(outerr, "") 

+

1897 

+

1898 def testReadFromStdin(self): 

+

1899 stdin = StringIO("--[[[cog cog.outl('Wow') ]]]\n--[[[end]]]\n") 

+

1900 def restore_stdin(old_stdin): 

+

1901 sys.stdin = old_stdin 

+

1902 self.addCleanup(restore_stdin, sys.stdin) 

+

1903 sys.stdin = stdin 

+

1904 

+

1905 stderr = StringIO() 

+

1906 self.cog.setOutput(stderr=stderr) 

+

1907 self.cog.callableMain(['argv0', '-']) 

+

1908 output = self.output.getvalue() 

+

1909 outerr = stderr.getvalue() 

+

1910 self.assertEqual(output, "--[[[cog cog.outl('Wow') ]]]\nWow\n--[[[end]]]\n") 

+

1911 self.assertEqual(outerr, "") 

+

1912 

+

1913 def testSuffixOutputLines(self): 

+

1914 d = { 

+

1915 'test.cog': """\ 

+

1916 Hey there. 

+

1917 ;[[[cog cog.outl('a\\nb\\n \\nc') ]]] 

+

1918 ;[[[end]]] 

+

1919 Good bye. 

+

1920 """, 

+

1921 

+

1922 'test.out': """\ 

+

1923 Hey there. 

+

1924 ;[[[cog cog.outl('a\\nb\\n \\nc') ]]] 

+

1925 a (foo) 

+

1926 b (foo) 

+

1927 """ # These three trailing spaces are important. 

+

1928 # The suffix is not applied to completely blank lines. 

+

1929 """ 

+

1930 c (foo) 

+

1931 ;[[[end]]] 

+

1932 Good bye. 

+

1933 """, 

+

1934 } 

+

1935 

+

1936 makeFiles(d) 

+

1937 self.cog.callableMain(['argv0', '-r', '-s', ' (foo)', 'test.cog']) 

+

1938 self.assertFilesSame('test.cog', 'test.out') 

+

1939 

+

1940 def testEmptySuffix(self): 

+

1941 d = { 

+

1942 'test.cog': """\ 

+

1943 ;[[[cog cog.outl('a\\nb\\nc') ]]] 

+

1944 ;[[[end]]] 

+

1945 """, 

+

1946 

+

1947 'test.out': """\ 

+

1948 ;[[[cog cog.outl('a\\nb\\nc') ]]] 

+

1949 a 

+

1950 b 

+

1951 c 

+

1952 ;[[[end]]] 

+

1953 """, 

+

1954 } 

+

1955 

+

1956 makeFiles(d) 

+

1957 self.cog.callableMain(['argv0', '-r', '-s', '', 'test.cog']) 

+

1958 self.assertFilesSame('test.cog', 'test.out') 

+

1959 

+

1960 def testHellishSuffix(self): 

+

1961 d = { 

+

1962 'test.cog': """\ 

+

1963 ;[[[cog cog.outl('a\\n\\nb') ]]] 

+

1964 """, 

+

1965 

+

1966 'test.out': """\ 

+

1967 ;[[[cog cog.outl('a\\n\\nb') ]]] 

+

1968 a /\\n*+([)]>< 

+

1969 

+

1970 b /\\n*+([)]>< 

+

1971 """, 

+

1972 } 

+

1973 

+

1974 makeFiles(d) 

+

1975 self.cog.callableMain(['argv0', '-z', '-r', '-s', r' /\n*+([)]><', 'test.cog']) 

+

1976 self.assertFilesSame('test.cog', 'test.out') 

+

1977 

+

1978 def testPrologue(self): 

+

1979 d = { 

+

1980 'test.cog': """\ 

+

1981 Some text. 

+

1982 //[[[cog cog.outl(str(math.sqrt(2))[:12])]]] 

+

1983 //[[[end]]] 

+

1984 epilogue. 

+

1985 """, 

+

1986 

+

1987 'test.out': """\ 

+

1988 Some text. 

+

1989 //[[[cog cog.outl(str(math.sqrt(2))[:12])]]] 

+

1990 1.4142135623 

+

1991 //[[[end]]] 

+

1992 epilogue. 

+

1993 """, 

+

1994 } 

+

1995 

+

1996 makeFiles(d) 

+

1997 self.cog.callableMain(['argv0', '-r', '-p', 'import math', 'test.cog']) 

+

1998 self.assertFilesSame('test.cog', 'test.out') 

+

1999 

+

2000 def testThreads(self): 

+

2001 # Test that the implictly imported cog module is actually different for 

+

2002 # different threads. 

+

2003 numthreads = 20 

+

2004 

+

2005 d = {} 

+

2006 for i in range(numthreads): 

+

2007 d['f{}.cog'.format(i)] = ( 

+

2008 "x\n" * i + 

+

2009 "[[[cog\n" + 

+

2010 "assert cog.firstLineNum == int(FIRST) == {}\n".format(i+1) + 

+

2011 "]]]\n" + 

+

2012 "[[[end]]]\n" 

+

2013 ) 

+

2014 makeFiles(d) 

+

2015 

+

2016 results = [] 

+

2017 

+

2018 def thread_main(num): 

+

2019 try: 

+

2020 ret = Cog().main( 

+

2021 ['cog.py', '-r', '-D', 'FIRST={}'.format(num+1), 'f{}.cog'.format(num)] 

+

2022 ) 

+

2023 assert ret == 0 

+

2024 except Exception as exc: 

+

2025 results.append(exc) 

+

2026 else: 

+

2027 results.append(None) 

+

2028 

+

2029 ts = [threading.Thread(target=thread_main, args=(i,)) for i in range(numthreads)] 

+

2030 for t in ts: 

+

2031 t.start() 

+

2032 for t in ts: 

+

2033 t.join() 

+

2034 assert results == [None] * numthreads 

+

2035 

+

2036 

+

2037class WritabilityTests(TestCaseWithTempDir): 

+

2038 

+

2039 d = { 

+

2040 'test.cog': """\ 

+

2041 //[[[cog 

+

2042 for fn in ['DoSomething', 'DoAnotherThing', 'DoLastThing']: 

+

2043 cog.outl("void %s();" % fn) 

+

2044 //]]] 

+

2045 //[[[end]]] 

+

2046 """, 

+

2047 

+

2048 'test.out': """\ 

+

2049 //[[[cog 

+

2050 for fn in ['DoSomething', 'DoAnotherThing', 'DoLastThing']: 

+

2051 cog.outl("void %s();" % fn) 

+

2052 //]]] 

+

2053 void DoSomething(); 

+

2054 void DoAnotherThing(); 

+

2055 void DoLastThing(); 

+

2056 //[[[end]]] 

+

2057 """, 

+

2058 } 

+

2059 

+

2060 if os.name == 'nt': #pragma: no cover 

+

2061 # for Windows 

+

2062 cmd_w_args = 'attrib -R %s' 

+

2063 cmd_w_asterisk = 'attrib -R *' 

+

2064 else: #pragma: no cover 

+

2065 # for unix-like 

+

2066 cmd_w_args = 'chmod +w %s' 

+

2067 cmd_w_asterisk = 'chmod +w *' 

+

2068 

+

2069 def setUp(self): 

+

2070 super(WritabilityTests, self).setUp() 

+

2071 makeFiles(self.d) 

+

2072 self.testcog = os.path.join(self.tempdir, 'test.cog') 

+

2073 os.chmod(self.testcog, stat.S_IREAD) # Make the file readonly. 

+

2074 assert not os.access(self.testcog, os.W_OK) 

+

2075 

+

2076 def tearDown(self): 

+

2077 os.chmod(self.testcog, stat.S_IWRITE) # Make the file writable again. 

+

2078 super(WritabilityTests, self).tearDown() 

+

2079 

+

2080 def testReadonlyNoCommand(self): 

+

2081 with self.assertRaisesRegex(CogError, "^Can't overwrite test.cog$"): 

+

2082 self.cog.callableMain(['argv0', '-r', 'test.cog']) 

+

2083 assert not os.access(self.testcog, os.W_OK) 

+

2084 

+

2085 def testReadonlyWithCommand(self): 

+

2086 self.cog.callableMain(['argv0', '-r', '-w', self.cmd_w_args, 'test.cog']) 

+

2087 self.assertFilesSame('test.cog', 'test.out') 

+

2088 assert os.access(self.testcog, os.W_OK) 

+

2089 

+

2090 def testReadonlyWithCommandWithNoSlot(self): 

+

2091 self.cog.callableMain(['argv0', '-r', '-w', self.cmd_w_asterisk, 'test.cog']) 

+

2092 self.assertFilesSame('test.cog', 'test.out') 

+

2093 assert os.access(self.testcog, os.W_OK) 

+

2094 

+

2095 def testReadonlyWithIneffectualCommand(self): 

+

2096 with self.assertRaisesRegex(CogError, "^Couldn't make test.cog writable$"): 

+

2097 self.cog.callableMain(['argv0', '-r', '-w', 'echo %s', 'test.cog']) 

+

2098 assert not os.access(self.testcog, os.W_OK) 

+

2099 

+

2100 

+

2101class ChecksumTests(TestCaseWithTempDir): 

+

2102 

+

2103 def testCreateChecksumOutput(self): 

+

2104 d = { 

+

2105 'cog1.txt': """\ 

+

2106 //[[[cog 

+

2107 cog.outl("This line was generated.") 

+

2108 //]]] 

+

2109 This line was generated. 

+

2110 //[[[end]]] 

+

2111 This line was not. 

+

2112 """, 

+

2113 

+

2114 'cog1.out': """\ 

+

2115 //[[[cog 

+

2116 cog.outl("This line was generated.") 

+

2117 //]]] 

+

2118 This line was generated. 

+

2119 //[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) 

+

2120 This line was not. 

+

2121 """, 

+

2122 } 

+

2123 

+

2124 makeFiles(d) 

+

2125 self.cog.callableMain(['argv0', '-r', '-c', 'cog1.txt']) 

+

2126 self.assertFilesSame('cog1.txt', 'cog1.out') 

+

2127 

+

2128 def testCheckChecksumOutput(self): 

+

2129 d = { 

+

2130 'cog1.txt': """\ 

+

2131 //[[[cog 

+

2132 cog.outl("This line was newly") 

+

2133 cog.outl("generated by cog") 

+

2134 cog.outl("blah blah.") 

+

2135 //]]] 

+

2136 This line was generated. 

+

2137 //[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) 

+

2138 """, 

+

2139 

+

2140 'cog1.out': """\ 

+

2141 //[[[cog 

+

2142 cog.outl("This line was newly") 

+

2143 cog.outl("generated by cog") 

+

2144 cog.outl("blah blah.") 

+

2145 //]]] 

+

2146 This line was newly 

+

2147 generated by cog 

+

2148 blah blah. 

+

2149 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2150 """, 

+

2151 } 

+

2152 

+

2153 makeFiles(d) 

+

2154 self.cog.callableMain(['argv0', '-r', '-c', 'cog1.txt']) 

+

2155 self.assertFilesSame('cog1.txt', 'cog1.out') 

+

2156 

+

2157 def testRemoveChecksumOutput(self): 

+

2158 d = { 

+

2159 'cog1.txt': """\ 

+

2160 //[[[cog 

+

2161 cog.outl("This line was newly") 

+

2162 cog.outl("generated by cog") 

+

2163 cog.outl("blah blah.") 

+

2164 //]]] 

+

2165 This line was generated. 

+

2166 //[[[end]]] (checksum: 8adb13fb59b996a1c7f0065ea9f3d893) fooey 

+

2167 """, 

+

2168 

+

2169 'cog1.out': """\ 

+

2170 //[[[cog 

+

2171 cog.outl("This line was newly") 

+

2172 cog.outl("generated by cog") 

+

2173 cog.outl("blah blah.") 

+

2174 //]]] 

+

2175 This line was newly 

+

2176 generated by cog 

+

2177 blah blah. 

+

2178 //[[[end]]] fooey 

+

2179 """, 

+

2180 } 

+

2181 

+

2182 makeFiles(d) 

+

2183 self.cog.callableMain(['argv0', '-r', 'cog1.txt']) 

+

2184 self.assertFilesSame('cog1.txt', 'cog1.out') 

+

2185 

+

2186 def testTamperedChecksumOutput(self): 

+

2187 d = { 

+

2188 'cog1.txt': """\ 

+

2189 //[[[cog 

+

2190 cog.outl("This line was newly") 

+

2191 cog.outl("generated by cog") 

+

2192 cog.outl("blah blah.") 

+

2193 //]]] 

+

2194 Xhis line was newly 

+

2195 generated by cog 

+

2196 blah blah. 

+

2197 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2198 """, 

+

2199 

+

2200 'cog2.txt': """\ 

+

2201 //[[[cog 

+

2202 cog.outl("This line was newly") 

+

2203 cog.outl("generated by cog") 

+

2204 cog.outl("blah blah.") 

+

2205 //]]] 

+

2206 This line was newly 

+

2207 generated by cog 

+

2208 blah blah! 

+

2209 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2210 """, 

+

2211 

+

2212 'cog3.txt': """\ 

+

2213 //[[[cog 

+

2214 cog.outl("This line was newly") 

+

2215 cog.outl("generated by cog") 

+

2216 cog.outl("blah blah.") 

+

2217 //]]] 

+

2218 

+

2219 This line was newly 

+

2220 generated by cog 

+

2221 blah blah. 

+

2222 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2223 """, 

+

2224 

+

2225 'cog4.txt': """\ 

+

2226 //[[[cog 

+

2227 cog.outl("This line was newly") 

+

2228 cog.outl("generated by cog") 

+

2229 cog.outl("blah blah.") 

+

2230 //]]] 

+

2231 This line was newly 

+

2232 generated by cog 

+

2233 blah blah.. 

+

2234 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2235 """, 

+

2236 

+

2237 'cog5.txt': """\ 

+

2238 //[[[cog 

+

2239 cog.outl("This line was newly") 

+

2240 cog.outl("generated by cog") 

+

2241 cog.outl("blah blah.") 

+

2242 //]]] 

+

2243 This line was newly 

+

2244 generated by cog 

+

2245 blah blah. 

+

2246 extra 

+

2247 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2248 """, 

+

2249 

+

2250 'cog6.txt': """\ 

+

2251 //[[[cog 

+

2252 cog.outl("This line was newly") 

+

2253 cog.outl("generated by cog") 

+

2254 cog.outl("blah blah.") 

+

2255 //]]] 

+

2256 //[[[end]]] (checksum: a8540982e5ad6b95c9e9a184b26f4346) 

+

2257 """, 

+

2258 } 

+

2259 

+

2260 makeFiles(d) 

+

2261 with self.assertRaisesRegex(CogError, 

+

2262 r"^cog1.txt\(9\): Output has been edited! Delete old checksum to unprotect.$"): 

+

2263 self.cog.callableMain(['argv0', '-c', "cog1.txt"]) 

+

2264 with self.assertRaisesRegex(CogError, 

+

2265 r"^cog2.txt\(9\): Output has been edited! Delete old checksum to unprotect.$"): 

+

2266 self.cog.callableMain(['argv0', '-c', "cog2.txt"]) 

+

2267 with self.assertRaisesRegex(CogError, 

+

2268 r"^cog3.txt\(10\): Output has been edited! Delete old checksum to unprotect.$"): 

+

2269 self.cog.callableMain(['argv0', '-c', "cog3.txt"]) 

+

2270 with self.assertRaisesRegex(CogError, 

+

2271 r"^cog4.txt\(9\): Output has been edited! Delete old checksum to unprotect.$"): 

+

2272 self.cog.callableMain(['argv0', '-c', "cog4.txt"]) 

+

2273 with self.assertRaisesRegex(CogError, 

+

2274 r"^cog5.txt\(10\): Output has been edited! Delete old checksum to unprotect.$"): 

+

2275 self.cog.callableMain(['argv0', '-c', "cog5.txt"]) 

+

2276 with self.assertRaisesRegex(CogError, 

+

2277 r"^cog6.txt\(6\): Output has been edited! Delete old checksum to unprotect.$"): 

+

2278 self.cog.callableMain(['argv0', '-c', "cog6.txt"]) 

+

2279 

+

2280 def testArgvIsntModified(self): 

+

2281 argv = ['argv0', '-v'] 

+

2282 orig_argv = argv[:] 

+

2283 self.cog.callableMain(argv) 

+

2284 self.assertEqual(argv, orig_argv) 

+

2285 

+

2286 

+

2287class CustomMarkerTests(TestCaseWithTempDir): 

+

2288 

+

2289 def testCustomerMarkers(self): 

+

2290 d = { 

+

2291 'test.cog': """\ 

+

2292 //{{ 

+

2293 cog.outl("void %s();" % "MyFunction") 

+

2294 //}} 

+

2295 //{{end}} 

+

2296 """, 

+

2297 

+

2298 'test.out': """\ 

+

2299 //{{ 

+

2300 cog.outl("void %s();" % "MyFunction") 

+

2301 //}} 

+

2302 void MyFunction(); 

+

2303 //{{end}} 

+

2304 """, 

+

2305 } 

+

2306 

+

2307 makeFiles(d) 

+

2308 self.cog.callableMain([ 

+

2309 'argv0', '-r', 

+

2310 '--markers={{ }} {{end}}', 

+

2311 'test.cog' 

+

2312 ]) 

+

2313 self.assertFilesSame('test.cog', 'test.out') 

+

2314 

+

2315 def testTrulyWackyMarkers(self): 

+

2316 # Make sure the markers are properly re-escaped. 

+

2317 d = { 

+

2318 'test.cog': """\ 

+

2319 //**( 

+

2320 cog.outl("void %s();" % "MyFunction") 

+

2321 //**) 

+

2322 //**(end)** 

+

2323 """, 

+

2324 

+

2325 'test.out': """\ 

+

2326 //**( 

+

2327 cog.outl("void %s();" % "MyFunction") 

+

2328 //**) 

+

2329 void MyFunction(); 

+

2330 //**(end)** 

+

2331 """, 

+

2332 } 

+

2333 

+

2334 makeFiles(d) 

+

2335 self.cog.callableMain([ 

+

2336 'argv0', '-r', 

+

2337 '--markers=**( **) **(end)**', 

+

2338 'test.cog' 

+

2339 ]) 

+

2340 self.assertFilesSame('test.cog', 'test.out') 

+

2341 

+

2342 def testChangeJustOneMarker(self): 

+

2343 d = { 

+

2344 'test.cog': """\ 

+

2345 //**( 

+

2346 cog.outl("void %s();" % "MyFunction") 

+

2347 //]]] 

+

2348 //[[[end]]] 

+

2349 """, 

+

2350 

+

2351 'test.out': """\ 

+

2352 //**( 

+

2353 cog.outl("void %s();" % "MyFunction") 

+

2354 //]]] 

+

2355 void MyFunction(); 

+

2356 //[[[end]]] 

+

2357 """, 

+

2358 } 

+

2359 

+

2360 makeFiles(d) 

+

2361 self.cog.callableMain([ 

+

2362 'argv0', '-r', 

+

2363 '--markers=**( ]]] [[[end]]]', 

+

2364 'test.cog' 

+

2365 ]) 

+

2366 self.assertFilesSame('test.cog', 'test.out') 

+

2367 

+

2368 

+

2369class BlakeTests(TestCaseWithTempDir): 

+

2370 

+

2371 # Blake Winton's contributions. 

+

2372 def testDeleteCode(self): 

+

2373 # -o sets the output file. 

+

2374 d = { 

+

2375 'test.cog': """\ 

+

2376 // This is my C++ file. 

+

2377 //[[[cog 

+

2378 fnames = ['DoSomething', 'DoAnotherThing', 'DoLastThing'] 

+

2379 for fn in fnames: 

+

2380 cog.outl("void %s();" % fn) 

+

2381 //]]] 

+

2382 Some Sample Code Here 

+

2383 //[[[end]]]Data Data 

+

2384 And Some More 

+

2385 """, 

+

2386 

+

2387 'test.out': """\ 

+

2388 // This is my C++ file. 

+

2389 void DoSomething(); 

+

2390 void DoAnotherThing(); 

+

2391 void DoLastThing(); 

+

2392 And Some More 

+

2393 """, 

+

2394 } 

+

2395 

+

2396 makeFiles(d) 

+

2397 self.cog.callableMain(['argv0', '-d', '-o', 'test.cogged', 'test.cog']) 

+

2398 self.assertFilesSame('test.cogged', 'test.out') 

+

2399 

+

2400 def testDeleteCodeWithDashRFails(self): 

+

2401 d = { 

+

2402 'test.cog': """\ 

+

2403 // This is my C++ file. 

+

2404 """ 

+

2405 } 

+

2406 

+

2407 makeFiles(d) 

+

2408 with self.assertRaisesRegex(CogUsageError, r"^Can't use -d with -r \(or you would delete all your source!\)$"): 

+

2409 self.cog.callableMain(['argv0', '-r', '-d', 'test.cog']) 

+

2410 

+

2411 def testSettingGlobals(self): 

+

2412 # Blake Winton contributed a way to set the globals that will be used in 

+

2413 # processFile(). 

+

2414 d = { 

+

2415 'test.cog': """\ 

+

2416 // This is my C++ file. 

+

2417 //[[[cog 

+

2418 for fn in fnames: 

+

2419 cog.outl("void %s();" % fn) 

+

2420 //]]] 

+

2421 Some Sample Code Here 

+

2422 //[[[end]]]""", 

+

2423 

+

2424 'test.out': """\ 

+

2425 // This is my C++ file. 

+

2426 void DoBlake(); 

+

2427 void DoWinton(); 

+

2428 void DoContribution(); 

+

2429 """, 

+

2430 } 

+

2431 

+

2432 makeFiles(d) 

+

2433 globals = {} 

+

2434 globals['fnames'] = ['DoBlake', 'DoWinton', 'DoContribution'] 

+

2435 self.cog.options.bDeleteCode = True 

+

2436 self.cog.processFile('test.cog', 'test.cogged', globals=globals) 

+

2437 self.assertFilesSame('test.cogged', 'test.out') 

+

2438 

+

2439 

+

2440class ErrorCallTests(TestCaseWithTempDir): 

+

2441 

+

2442 def testErrorCallHasNoTraceback(self): 

+

2443 # Test that cog.error() doesn't show a traceback. 

+

2444 d = { 

+

2445 'error.cog': """\ 

+

2446 //[[[cog 

+

2447 cog.error("Something Bad!") 

+

2448 //]]] 

+

2449 //[[[end]]] 

+

2450 """, 

+

2451 } 

+

2452 

+

2453 makeFiles(d) 

+

2454 self.cog.main(['argv0', '-r', 'error.cog']) 

+

2455 output = self.output.getvalue() 

+

2456 self.assertEqual(output, "Cogging error.cog\nError: Something Bad!\n") 

+

2457 

+

2458 def testRealErrorHasTraceback(self): 

+

2459 # Test that a genuine error does show a traceback. 

+

2460 d = { 

+

2461 'error.cog': """\ 

+

2462 //[[[cog 

+

2463 raise RuntimeError("Hey!") 

+

2464 //]]] 

+

2465 //[[[end]]] 

+

2466 """, 

+

2467 } 

+

2468 

+

2469 makeFiles(d) 

+

2470 self.cog.main(['argv0', '-r', 'error.cog']) 

+

2471 output = self.output.getvalue() 

+

2472 msg = 'Actual output:\n' + output 

+

2473 self.assertTrue(output.startswith("Cogging error.cog\nTraceback (most recent"), msg) 

+

2474 self.assertIn("RuntimeError: Hey!", output) 

+

2475 

+

2476 

+

2477# Things not yet tested: 

+

2478# - A bad -w command (currently fails silently). 

- - diff --git a/doc/sample_html/cogapp_test_makefiles_py.html b/doc/sample_html/cogapp_test_makefiles_py.html index c9c123dd8..4a9f43549 100644 --- a/doc/sample_html/cogapp_test_makefiles_py.html +++ b/doc/sample_html/cogapp_test_makefiles_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/test_makefiles.py: 100.00% + Coverage for cogapp/test_makefiles.py: 23.38% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,212 +51,137 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

- -
-

""" Test the cogapp.makefiles modules 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2015, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import 

-

 

-

import unittest 

-

import shutil, os, random, tempfile 

-

 

-

from . import makefiles 

-

 

-

 

-

class SimpleTests(unittest.TestCase): 

-

 

-

def setUp(self): 

-

# Create a temporary directory. 

-

my_dir = 'testmakefiles_tempdir_' + str(random.random())[2:] 

-

self.tempdir = os.path.join(tempfile.gettempdir(), my_dir) 

-

os.mkdir(self.tempdir) 

-

 

-

def tearDown(self): 

-

# Get rid of the temporary directory. 

-

shutil.rmtree(self.tempdir) 

-

 

-

def exists(self, dname, fname): 

-

return os.path.exists(os.path.join(dname, fname)) 

-

 

-

def checkFilesExist(self, d, dname): 

-

for fname in d.keys(): 

-

assert(self.exists(dname, fname)) 

-

if type(d[fname]) == type({}): 

-

self.checkFilesExist(d[fname], os.path.join(dname, fname)) 

-

 

-

def checkFilesDontExist(self, d, dname): 

-

for fname in d.keys(): 

-

assert(not self.exists(dname, fname)) 

-

 

-

def testOneFile(self): 

-

fname = 'foo.txt' 

-

notfname = 'not_here.txt' 

-

d = { fname: "howdy" } 

-

assert(not self.exists(self.tempdir, fname)) 

-

assert(not self.exists(self.tempdir, notfname)) 

-

 

-

makefiles.makeFiles(d, self.tempdir) 

-

assert(self.exists(self.tempdir, fname)) 

-

assert(not self.exists(self.tempdir, notfname)) 

-

 

-

makefiles.removeFiles(d, self.tempdir) 

-

assert(not self.exists(self.tempdir, fname)) 

-

assert(not self.exists(self.tempdir, notfname)) 

-

 

-

def testManyFiles(self): 

-

d = { 

-

'top1.txt': "howdy", 

-

'top2.txt': "hello", 

-

'sub': { 

-

'sub1.txt': "inside", 

-

'sub2.txt': "inside2" 

-

}, 

-

} 

-

 

-

self.checkFilesDontExist(d, self.tempdir) 

-

makefiles.makeFiles(d, self.tempdir) 

-

self.checkFilesExist(d, self.tempdir) 

-

makefiles.removeFiles(d, self.tempdir) 

-

self.checkFilesDontExist(d, self.tempdir) 

-

 

-

def testContents(self): 

-

fname = 'bar.txt' 

-

cont0 = "I am bar.txt" 

-

d = { fname: cont0 } 

-

makefiles.makeFiles(d, self.tempdir) 

-

fcont1 = open(os.path.join(self.tempdir, fname)) 

-

assert(fcont1.read() == cont0) 

-

fcont1.close() 

-

 

-

def testDedent(self): 

-

fname = 'dedent.txt' 

-

d = { fname: """\ 

-

This is dedent.txt 

-

\tTabbed in. 

-

spaced in. 

-

OK. 

-

""" 

-

} 

-

makefiles.makeFiles(d, self.tempdir) 

-

fcont = open(os.path.join(self.tempdir, fname)) 

-

assert(fcont.read() == "This is dedent.txt\n\tTabbed in.\n spaced in.\nOK.\n") 

-

fcont.close() 

- -
+

1""" Test the cogapp.makefiles modules 

+

2 http://nedbatchelder.com/code/cog 

+

3 

+

4 Copyright 2004-2019, Ned Batchelder. 

+

5""" 

+

6 

+

7from __future__ import absolute_import 

+

8 

+

9import shutil 

+

10import os 

+

11import random 

+

12import tempfile 

+

13 

+

14from . import makefiles 

+

15from .backward import TestCase 

+

16 

+

17 

+

18class SimpleTests(TestCase): 

+

19 

+

20 def setUp(self): 

+

21 # Create a temporary directory. 

+

22 my_dir = 'testmakefiles_tempdir_' + str(random.random())[2:] 

+

23 self.tempdir = os.path.join(tempfile.gettempdir(), my_dir) 

+

24 os.mkdir(self.tempdir) 

+

25 

+

26 def tearDown(self): 

+

27 # Get rid of the temporary directory. 

+

28 shutil.rmtree(self.tempdir) 

+

29 

+

30 def exists(self, dname, fname): 

+

31 return os.path.exists(os.path.join(dname, fname)) 

+

32 

+

33 def checkFilesExist(self, d, dname): 

+

34 for fname in d.keys(): 

+

35 assert(self.exists(dname, fname)) 

+

36 if type(d[fname]) == type({}): 

+

37 self.checkFilesExist(d[fname], os.path.join(dname, fname)) 

+

38 

+

39 def checkFilesDontExist(self, d, dname): 

+

40 for fname in d.keys(): 

+

41 assert(not self.exists(dname, fname)) 

+

42 

+

43 def testOneFile(self): 

+

44 fname = 'foo.txt' 

+

45 notfname = 'not_here.txt' 

+

46 d = { fname: "howdy" } 

+

47 assert(not self.exists(self.tempdir, fname)) 

+

48 assert(not self.exists(self.tempdir, notfname)) 

+

49 

+

50 makefiles.makeFiles(d, self.tempdir) 

+

51 assert(self.exists(self.tempdir, fname)) 

+

52 assert(not self.exists(self.tempdir, notfname)) 

+

53 

+

54 makefiles.removeFiles(d, self.tempdir) 

+

55 assert(not self.exists(self.tempdir, fname)) 

+

56 assert(not self.exists(self.tempdir, notfname)) 

+

57 

+

58 def testManyFiles(self): 

+

59 d = { 

+

60 'top1.txt': "howdy", 

+

61 'top2.txt': "hello", 

+

62 'sub': { 

+

63 'sub1.txt': "inside", 

+

64 'sub2.txt': "inside2", 

+

65 }, 

+

66 } 

+

67 

+

68 self.checkFilesDontExist(d, self.tempdir) 

+

69 makefiles.makeFiles(d, self.tempdir) 

+

70 self.checkFilesExist(d, self.tempdir) 

+

71 makefiles.removeFiles(d, self.tempdir) 

+

72 self.checkFilesDontExist(d, self.tempdir) 

+

73 

+

74 def testOverlapping(self): 

+

75 d1 = { 

+

76 'top1.txt': "howdy", 

+

77 'sub': { 

+

78 'sub1.txt': "inside", 

+

79 }, 

+

80 } 

+

81 

+

82 d2 = { 

+

83 'top2.txt': "hello", 

+

84 'sub': { 

+

85 'sub2.txt': "inside2", 

+

86 }, 

+

87 } 

+

88 

+

89 self.checkFilesDontExist(d1, self.tempdir) 

+

90 self.checkFilesDontExist(d2, self.tempdir) 

+

91 makefiles.makeFiles(d1, self.tempdir) 

+

92 makefiles.makeFiles(d2, self.tempdir) 

+

93 self.checkFilesExist(d1, self.tempdir) 

+

94 self.checkFilesExist(d2, self.tempdir) 

+

95 makefiles.removeFiles(d1, self.tempdir) 

+

96 makefiles.removeFiles(d2, self.tempdir) 

+

97 self.checkFilesDontExist(d1, self.tempdir) 

+

98 self.checkFilesDontExist(d2, self.tempdir) 

+

99 

+

100 def testContents(self): 

+

101 fname = 'bar.txt' 

+

102 cont0 = "I am bar.txt" 

+

103 d = { fname: cont0 } 

+

104 makefiles.makeFiles(d, self.tempdir) 

+

105 fcont1 = open(os.path.join(self.tempdir, fname)) 

+

106 assert(fcont1.read() == cont0) 

+

107 fcont1.close() 

+

108 

+

109 def testDedent(self): 

+

110 fname = 'dedent.txt' 

+

111 d = { 

+

112 fname: """\ 

+

113 This is dedent.txt 

+

114 \tTabbed in. 

+

115 spaced in. 

+

116 OK. 

+

117 """, 

+

118 } 

+

119 makefiles.makeFiles(d, self.tempdir) 

+

120 fcont = open(os.path.join(self.tempdir, fname)) 

+

121 assert(fcont.read() == "This is dedent.txt\n\tTabbed in.\n spaced in.\nOK.\n") 

+

122 fcont.close() 

- - diff --git a/doc/sample_html/cogapp_test_whiteutils_py.html b/doc/sample_html/cogapp_test_whiteutils_py.html index 839d199c3..ae853ac14 100644 --- a/doc/sample_html/cogapp_test_whiteutils_py.html +++ b/doc/sample_html/cogapp_test_whiteutils_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/test_whiteutils.py: 100.00% + Coverage for cogapp/test_whiteutils.py: 27.54% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,232 +51,116 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

-

72

-

73

-

74

-

75

-

76

-

77

-

78

-

79

-

80

-

81

-

82

-

83

-

84

-

85

-

86

-

87

-

88

-

89

-

90

-

91

-

92

-

93

-

94

-

95

-

96

-

97

-

98

-

99

-

100

-

101

-

102

- -
-

""" Test the cogapp.whiteutils module. 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2015, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import 

-

 

-

import unittest 

-

 

-

from .whiteutils import * 

-

 

-

 

-

class WhitePrefixTests(unittest.TestCase): 

-

""" Test cases for cogapp.whiteutils. 

-

""" 

-

def testSingleLine(self): 

-

self.assertEqual(whitePrefix(['']), '') 

-

self.assertEqual(whitePrefix([' ']), '') 

-

self.assertEqual(whitePrefix(['x']), '') 

-

self.assertEqual(whitePrefix([' x']), ' ') 

-

self.assertEqual(whitePrefix(['\tx']), '\t') 

-

self.assertEqual(whitePrefix([' x']), ' ') 

-

self.assertEqual(whitePrefix([' \t \tx ']), ' \t \t') 

-

 

-

def testMultiLine(self): 

-

self.assertEqual(whitePrefix([' x',' x',' x']), ' ') 

-

self.assertEqual(whitePrefix([' y',' y',' y']), ' ') 

-

self.assertEqual(whitePrefix([' y',' y',' y']), ' ') 

-

 

-

def testBlankLinesAreIgnored(self): 

-

self.assertEqual(whitePrefix([' x',' x','',' x']), ' ') 

-

self.assertEqual(whitePrefix(['',' x',' x',' x']), ' ') 

-

self.assertEqual(whitePrefix([' x',' x',' x','']), ' ') 

-

self.assertEqual(whitePrefix([' x',' x',' ',' x']), ' ') 

-

 

-

def testTabCharacters(self): 

-

self.assertEqual(whitePrefix(['\timport sys', '', '\tprint sys.argv']), '\t') 

-

 

-

def testDecreasingLengths(self): 

-

self.assertEqual(whitePrefix([' x',' x',' x']), ' ') 

-

self.assertEqual(whitePrefix([' x',' x',' x']), ' ') 

-

 

-

 

-

class ReindentBlockTests(unittest.TestCase): 

-

""" Test cases for cogapp.reindentBlock. 

-

""" 

-

def testNonTermLine(self): 

-

self.assertEqual(reindentBlock(''), '') 

-

self.assertEqual(reindentBlock('x'), 'x') 

-

self.assertEqual(reindentBlock(' x'), 'x') 

-

self.assertEqual(reindentBlock(' x'), 'x') 

-

self.assertEqual(reindentBlock('\tx'), 'x') 

-

self.assertEqual(reindentBlock('x', ' '), ' x') 

-

self.assertEqual(reindentBlock('x', '\t'), '\tx') 

-

self.assertEqual(reindentBlock(' x', ' '), ' x') 

-

self.assertEqual(reindentBlock(' x', '\t'), '\tx') 

-

self.assertEqual(reindentBlock(' x', ' '), ' x') 

-

 

-

def testSingleLine(self): 

-

self.assertEqual(reindentBlock('\n'), '\n') 

-

self.assertEqual(reindentBlock('x\n'), 'x\n') 

-

self.assertEqual(reindentBlock(' x\n'), 'x\n') 

-

self.assertEqual(reindentBlock(' x\n'), 'x\n') 

-

self.assertEqual(reindentBlock('\tx\n'), 'x\n') 

-

self.assertEqual(reindentBlock('x\n', ' '), ' x\n') 

-

self.assertEqual(reindentBlock('x\n', '\t'), '\tx\n') 

-

self.assertEqual(reindentBlock(' x\n', ' '), ' x\n') 

-

self.assertEqual(reindentBlock(' x\n', '\t'), '\tx\n') 

-

self.assertEqual(reindentBlock(' x\n', ' '), ' x\n') 

-

 

-

def testRealBlock(self): 

-

self.assertEqual( 

-

reindentBlock('\timport sys\n\n\tprint sys.argv\n'), 

-

'import sys\n\nprint sys.argv\n' 

-

) 

-

 

-

 

-

class CommonPrefixTests(unittest.TestCase): 

-

""" Test cases for cogapp.commonPrefix. 

-

""" 

-

def testDegenerateCases(self): 

-

self.assertEqual(commonPrefix([]), '') 

-

self.assertEqual(commonPrefix(['']), '') 

-

self.assertEqual(commonPrefix(['','','','','']), '') 

-

self.assertEqual(commonPrefix(['cat in the hat']), 'cat in the hat') 

-

 

-

def testNoCommonPrefix(self): 

-

self.assertEqual(commonPrefix(['a','b']), '') 

-

self.assertEqual(commonPrefix(['a','b','c','d','e','f']), '') 

-

self.assertEqual(commonPrefix(['a','a','a','a','a','x']), '') 

-

 

-

def testUsualCases(self): 

-

self.assertEqual(commonPrefix(['ab', 'ac']), 'a') 

-

self.assertEqual(commonPrefix(['aab', 'aac']), 'aa') 

-

self.assertEqual(commonPrefix(['aab', 'aab', 'aab', 'aac']), 'aa') 

-

 

-

def testBlankLine(self): 

-

self.assertEqual(commonPrefix(['abc', 'abx', '', 'aby']), '') 

-

 

-

def testDecreasingLengths(self): 

-

self.assertEqual(commonPrefix(['abcd', 'abc', 'ab']), 'ab') 

- -
+

1""" Test the cogapp.whiteutils module. 

+

2 http://nedbatchelder.com/code/cog 

+

3 

+

4 Copyright 2004-2019, Ned Batchelder. 

+

5""" 

+

6 

+

7from __future__ import absolute_import 

+

8 

+

9from .backward import TestCase 

+

10from .whiteutils import * 

+

11 

+

12 

+

13class WhitePrefixTests(TestCase): 

+

14 """ Test cases for cogapp.whiteutils. 

+

15 """ 

+

16 def testSingleLine(self): 

+

17 self.assertEqual(whitePrefix(['']), '') 

+

18 self.assertEqual(whitePrefix([' ']), '') 

+

19 self.assertEqual(whitePrefix(['x']), '') 

+

20 self.assertEqual(whitePrefix([' x']), ' ') 

+

21 self.assertEqual(whitePrefix(['\tx']), '\t') 

+

22 self.assertEqual(whitePrefix([' x']), ' ') 

+

23 self.assertEqual(whitePrefix([' \t \tx ']), ' \t \t') 

+

24 

+

25 def testMultiLine(self): 

+

26 self.assertEqual(whitePrefix([' x',' x',' x']), ' ') 

+

27 self.assertEqual(whitePrefix([' y',' y',' y']), ' ') 

+

28 self.assertEqual(whitePrefix([' y',' y',' y']), ' ') 

+

29 

+

30 def testBlankLinesAreIgnored(self): 

+

31 self.assertEqual(whitePrefix([' x',' x','',' x']), ' ') 

+

32 self.assertEqual(whitePrefix(['',' x',' x',' x']), ' ') 

+

33 self.assertEqual(whitePrefix([' x',' x',' x','']), ' ') 

+

34 self.assertEqual(whitePrefix([' x',' x',' ',' x']), ' ') 

+

35 

+

36 def testTabCharacters(self): 

+

37 self.assertEqual(whitePrefix(['\timport sys', '', '\tprint sys.argv']), '\t') 

+

38 

+

39 def testDecreasingLengths(self): 

+

40 self.assertEqual(whitePrefix([' x',' x',' x']), ' ') 

+

41 self.assertEqual(whitePrefix([' x',' x',' x']), ' ') 

+

42 

+

43 

+

44class ReindentBlockTests(TestCase): 

+

45 """ Test cases for cogapp.reindentBlock. 

+

46 """ 

+

47 def testNonTermLine(self): 

+

48 self.assertEqual(reindentBlock(''), '') 

+

49 self.assertEqual(reindentBlock('x'), 'x') 

+

50 self.assertEqual(reindentBlock(' x'), 'x') 

+

51 self.assertEqual(reindentBlock(' x'), 'x') 

+

52 self.assertEqual(reindentBlock('\tx'), 'x') 

+

53 self.assertEqual(reindentBlock('x', ' '), ' x') 

+

54 self.assertEqual(reindentBlock('x', '\t'), '\tx') 

+

55 self.assertEqual(reindentBlock(' x', ' '), ' x') 

+

56 self.assertEqual(reindentBlock(' x', '\t'), '\tx') 

+

57 self.assertEqual(reindentBlock(' x', ' '), ' x') 

+

58 

+

59 def testSingleLine(self): 

+

60 self.assertEqual(reindentBlock('\n'), '\n') 

+

61 self.assertEqual(reindentBlock('x\n'), 'x\n') 

+

62 self.assertEqual(reindentBlock(' x\n'), 'x\n') 

+

63 self.assertEqual(reindentBlock(' x\n'), 'x\n') 

+

64 self.assertEqual(reindentBlock('\tx\n'), 'x\n') 

+

65 self.assertEqual(reindentBlock('x\n', ' '), ' x\n') 

+

66 self.assertEqual(reindentBlock('x\n', '\t'), '\tx\n') 

+

67 self.assertEqual(reindentBlock(' x\n', ' '), ' x\n') 

+

68 self.assertEqual(reindentBlock(' x\n', '\t'), '\tx\n') 

+

69 self.assertEqual(reindentBlock(' x\n', ' '), ' x\n') 

+

70 

+

71 def testRealBlock(self): 

+

72 self.assertEqual( 

+

73 reindentBlock('\timport sys\n\n\tprint sys.argv\n'), 

+

74 'import sys\n\nprint sys.argv\n' 

+

75 ) 

+

76 

+

77 

+

78class CommonPrefixTests(TestCase): 

+

79 """ Test cases for cogapp.commonPrefix. 

+

80 """ 

+

81 def testDegenerateCases(self): 

+

82 self.assertEqual(commonPrefix([]), '') 

+

83 self.assertEqual(commonPrefix(['']), '') 

+

84 self.assertEqual(commonPrefix(['','','','','']), '') 

+

85 self.assertEqual(commonPrefix(['cat in the hat']), 'cat in the hat') 

+

86 

+

87 def testNoCommonPrefix(self): 

+

88 self.assertEqual(commonPrefix(['a','b']), '') 

+

89 self.assertEqual(commonPrefix(['a','b','c','d','e','f']), '') 

+

90 self.assertEqual(commonPrefix(['a','a','a','a','a','x']), '') 

+

91 

+

92 def testUsualCases(self): 

+

93 self.assertEqual(commonPrefix(['ab', 'ac']), 'a') 

+

94 self.assertEqual(commonPrefix(['aab', 'aac']), 'aa') 

+

95 self.assertEqual(commonPrefix(['aab', 'aab', 'aab', 'aac']), 'aa') 

+

96 

+

97 def testBlankLine(self): 

+

98 self.assertEqual(commonPrefix(['abc', 'abx', '', 'aby']), '') 

+

99 

+

100 def testDecreasingLengths(self): 

+

101 self.assertEqual(commonPrefix(['abcd', 'abc', 'ab']), 'ab') 

- - diff --git a/doc/sample_html/cogapp_whiteutils_py.html b/doc/sample_html/cogapp_whiteutils_py.html index 7d714ca70..d91bff54c 100644 --- a/doc/sample_html/cogapp_whiteutils_py.html +++ b/doc/sample_html/cogapp_whiteutils_py.html @@ -1,16 +1,10 @@ - - - - - - Coverage for cogapp/whiteutils.py: 98.70% + Coverage for cogapp/whiteutils.py: 88.61% - @@ -20,28 +14,21 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -64,170 +51,88 @@

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

-

33

-

34

-

35

-

36

-

37

-

38

-

39

-

40

-

41

-

42

-

43

-

44

-

45

-

46

-

47

-

48

-

49

-

50

-

51

-

52

-

53

-

54

-

55

-

56

-

57

-

58

-

59

-

60

-

61

-

62

-

63

-

64

-

65

-

66

-

67

-

68

-

69

-

70

-

71

- -
-

""" Indentation utilities for Cog. 

-

http://nedbatchelder.com/code/cog 

-

 

-

Copyright 2004-2015, Ned Batchelder. 

-

""" 

-

 

-

from __future__ import absolute_import 

-

import re 

-

from .backward import string_types, bytes_types, to_bytes, b 

-

 

-

def whitePrefix(strings): 

-

""" Determine the whitespace prefix common to all non-blank lines 

-

in the argument list. 

-

""" 

-

# Remove all blank lines from the list 

-

strings = [s for s in strings if s.strip() != ''] 

-

 

-

if not strings: return '' 

-

 

-

# Find initial whitespace chunk in the first line. 

-

# This is the best prefix we can hope for. 

-

pat = r'\s*' 

-

23 ↛ 25line 23 didn't jump to line 25, because the condition on line 23 was never false if isinstance(strings[0], bytes_types): 

-

pat = to_bytes(pat) 

-

prefix = re.match(pat, strings[0]).group(0) 

-

 

-

# Loop over the other strings, keeping only as much of 

-

# the prefix as matches each string. 

-

for s in strings: 

-

for i in range(len(prefix)): 

-

if prefix[i] != s[i]: 

-

prefix = prefix[:i] 

-

break 

-

return prefix 

-

 

-

def reindentBlock(lines, newIndent=''): 

-

""" Take a block of text as a string or list of lines. 

-

Remove any common whitespace indentation. 

-

Re-indent using newIndent, and return it as a single string. 

-

""" 

-

sep, nothing = '\n', '' 

-

if isinstance(lines, bytes_types): 

-

sep, nothing = b('\n'), b('') 

-

if isinstance(lines, string_types): 

-

lines = lines.split(sep) 

-

oldIndent = whitePrefix(lines) 

-

outLines = [] 

-

for l in lines: 

-

if oldIndent: 

-

l = l.replace(oldIndent, nothing, 1) 

-

if l and newIndent: 

-

l = newIndent + l 

-

outLines.append(l) 

-

return sep.join(outLines) 

-

 

-

def commonPrefix(strings): 

-

""" Find the longest string that is a prefix of all the strings. 

-

""" 

-

if not strings: 

-

return '' 

-

prefix = strings[0] 

-

for s in strings: 

-

if len(s) < len(prefix): 

-

prefix = prefix[:len(s)] 

-

if not prefix: 

-

return '' 

-

for i in range(len(prefix)): 

-

if prefix[i] != s[i]: 

-

prefix = prefix[:i] 

-

break 

-

return prefix 

- -
+

1""" Indentation utilities for Cog. 

+

2 http://nedbatchelder.com/code/cog 

+

3 

+

4 Copyright 2004-2019, Ned Batchelder. 

+

5""" 

+

6 

+

7from __future__ import absolute_import 

+

8 

+

9import re 

+

10 

+

11from .backward import string_types, bytes_types, to_bytes 

+

12 

+

13def whitePrefix(strings): 

+

14 """ Determine the whitespace prefix common to all non-blank lines 

+

15 in the argument list. 

+

16 """ 

+

17 # Remove all blank lines from the list 

+

18 strings = [s for s in strings if s.strip() != ''] 

+

19 

+

20 if not strings: return '' 

+

21 

+

22 # Find initial whitespace chunk in the first line. 

+

23 # This is the best prefix we can hope for. 

+

24 pat = r'\s*' 

+

25 if isinstance(strings[0], bytes_types): 25 ↛ 26line 25 didn't jump to line 26, because the condition on line 25 was never true

+

26 pat = to_bytes(pat) 

+

27 prefix = re.match(pat, strings[0]).group(0) 

+

28 

+

29 # Loop over the other strings, keeping only as much of 

+

30 # the prefix as matches each string. 

+

31 for s in strings: 

+

32 for i in range(len(prefix)): 

+

33 if prefix[i] != s[i]: 33 ↛ 34line 33 didn't jump to line 34, because the condition on line 33 was never true

+

34 prefix = prefix[:i] 

+

35 break 

+

36 return prefix 

+

37 

+

38def reindentBlock(lines, newIndent=''): 

+

39 """ Take a block of text as a string or list of lines. 

+

40 Remove any common whitespace indentation. 

+

41 Re-indent using newIndent, and return it as a single string. 

+

42 """ 

+

43 sep, nothing = '\n', '' 

+

44 if isinstance(lines, bytes_types): 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true

+

45 sep, nothing = b'\n', b'' 

+

46 if isinstance(lines, string_types): 

+

47 lines = lines.split(sep) 

+

48 oldIndent = whitePrefix(lines) 

+

49 outLines = [] 

+

50 for l in lines: 

+

51 if oldIndent: 

+

52 l = l.replace(oldIndent, nothing, 1) 

+

53 if l and newIndent: 

+

54 l = newIndent + l 

+

55 outLines.append(l) 

+

56 return sep.join(outLines) 

+

57 

+

58def commonPrefix(strings): 

+

59 """ Find the longest string that is a prefix of all the strings. 

+

60 """ 

+

61 if not strings: 61 ↛ 62line 61 didn't jump to line 62, because the condition on line 61 was never true

+

62 return '' 

+

63 prefix = strings[0] 

+

64 for s in strings: 

+

65 if len(s) < len(prefix): 

+

66 prefix = prefix[:len(s)] 

+

67 if not prefix: 

+

68 return '' 

+

69 for i in range(len(prefix)): 

+

70 if prefix[i] != s[i]: 

+

71 prefix = prefix[:i] 

+

72 break 

+

73 return prefix 

- - diff --git a/doc/sample_html/coverage_html.js b/doc/sample_html/coverage_html.js index f6f5de207..3bf04bf92 100644 --- a/doc/sample_html/coverage_html.js +++ b/doc/sample_html/coverage_html.js @@ -1,5 +1,5 @@ // Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -// For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt // Coverage.py HTML report browser code. /*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ @@ -169,22 +169,16 @@ coverage.wire_up_filter = function () { // Loaded on index.html coverage.index_ready = function ($) { - // Look for a cookie containing previous sort settings: + // Look for a localStorage item containing previous sort settings: var sort_list = []; - var cookie_name = "COVERAGE_INDEX_SORT"; - var i; - - // This almost makes it worth installing the jQuery cookie plugin: - if (document.cookie.indexOf(cookie_name) > -1) { - var cookies = document.cookie.split(";"); - for (i = 0; i < cookies.length; i++) { - var parts = cookies[i].split("="); - - if ($.trim(parts[0]) === cookie_name && parts[1]) { - sort_list = eval("[[" + parts[1] + "]]"); - break; - } - } + var storage_name = "COVERAGE_INDEX_SORT"; + var stored_list = undefined; + try { + stored_list = localStorage.getItem(storage_name); + } catch(err) {} + + if (stored_list) { + sort_list = JSON.parse('[[' + stored_list + ']]'); } // Create a new widget which exists only to save and restore @@ -231,7 +225,9 @@ coverage.index_ready = function ($) { // Watch for page unload events so we can save the final sort settings: $(window).unload(function () { - document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + try { + localStorage.setItem(storage_name, sort_list.toString()) + } catch(err) {} }); }; @@ -240,7 +236,7 @@ coverage.index_ready = function ($) { coverage.pyfile_ready = function ($) { // If we're directed to a particular line number, highlight the line. var frag = location.hash; - if (frag.length > 2 && frag[1] === 'n') { + if (frag.length > 2 && frag[1] === 't') { $(frag).addClass('highlight'); coverage.set_sel(parseInt(frag.substr(2), 10)); } @@ -265,21 +261,22 @@ coverage.pyfile_ready = function ($) { coverage.init_scroll_markers(); - // Rebuild scroll markers after window high changing - $(window).resize(coverage.resize_scroll_markers); + // Rebuild scroll markers when the window height changes. + $(window).resize(coverage.build_scroll_markers); }; coverage.toggle_lines = function (btn, cls) { btn = $(btn); - var hide = "hide_"+cls; - if (btn.hasClass(hide)) { - $("#source ."+cls).removeClass(hide); - btn.removeClass(hide); + var show = "show_"+cls; + if (btn.hasClass(show)) { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); } else { - $("#source ."+cls).addClass(hide); - btn.addClass(hide); + $("#source ." + cls).addClass(show); + btn.addClass(show); } + coverage.build_scroll_markers(); }; // Return the nth line div. @@ -292,11 +289,6 @@ coverage.num_elt = function (n) { return $("#n" + n); }; -// Return the container of all the code. -coverage.code_container = function () { - return $(".linenos"); -}; - // Set the selection. b and e are line numbers. coverage.set_sel = function (b, e) { // The first line selected. @@ -315,9 +307,17 @@ coverage.to_first_chunk = function () { coverage.to_next_chunk(); }; -coverage.is_transparent = function (color) { - // Different browsers return different colors for "none". - return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + var klass = line_elt.attr('class'); + if (klass) { + var m = klass.match(/\bshow_\w+\b/); + if (m) { + return m[0]; + } + } + return null; }; coverage.to_next_chunk = function () { @@ -325,14 +325,14 @@ coverage.to_next_chunk = function () { // Find the start of the next colored chunk. var probe = c.sel_end; - var color, probe_line; + var chunk_indicator, probe_line; while (true) { probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } - color = probe_line.css("background-color"); - if (!c.is_transparent(color)) { + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { break; } probe++; @@ -342,11 +342,11 @@ coverage.to_next_chunk = function () { var begin = probe; // Find the end of this chunk. - var next_color = color; - while (next_color === color) { + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { probe++; probe_line = c.line_elt(probe); - next_color = probe_line.css("background-color"); + next_indicator = c.chunk_indicator(probe_line); } c.set_sel(begin, probe); c.show_selection(); @@ -361,25 +361,25 @@ coverage.to_prev_chunk = function () { if (probe_line.length === 0) { return; } - var color = probe_line.css("background-color"); - while (probe > 0 && c.is_transparent(color)) { + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 0 && !chunk_indicator) { probe--; probe_line = c.line_elt(probe); if (probe_line.length === 0) { return; } - color = probe_line.css("background-color"); + chunk_indicator = c.chunk_indicator(probe_line); } // There's a prev chunk, `probe` points to its last line. var end = probe+1; // Find the beginning of this chunk. - var prev_color = color; - while (prev_color === color) { + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { probe--; probe_line = c.line_elt(probe); - prev_color = probe_line.css("background-color"); + prev_indicator = c.chunk_indicator(probe_line); } c.set_sel(probe+1, end); c.show_selection(); @@ -451,29 +451,29 @@ coverage.select_line_or_chunk = function (lineno) { if (probe_line.length === 0) { return; } - var the_color = probe_line.css("background-color"); - if (!c.is_transparent(the_color)) { + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { // The line is in a highlighted chunk. // Search backward for the first line. var probe = lineno; - var color = the_color; - while (probe > 0 && color === the_color) { + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { probe--; probe_line = c.line_elt(probe); if (probe_line.length === 0) { break; } - color = probe_line.css("background-color"); + indicator = c.chunk_indicator(probe_line); } var begin = probe + 1; // Search forward for the last line. probe = lineno; - color = the_color; - while (color === the_color) { + indicator = the_indicator; + while (indicator === the_indicator) { probe++; probe_line = c.line_elt(probe); - color = probe_line.css("background-color"); + indicator = c.chunk_indicator(probe_line); } coverage.set_sel(begin, probe); @@ -487,7 +487,7 @@ coverage.show_selection = function () { var c = coverage; // Highlight the lines in the chunk - c.code_container().find(".highlight").removeClass("highlight"); + $(".linenos .highlight").removeClass("highlight"); for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { c.num_elt(probe).addClass("highlight"); } @@ -517,21 +517,21 @@ coverage.finish_scrolling = function () { coverage.init_scroll_markers = function () { var c = coverage; // Init some variables - c.lines_len = $('td.text p').length; + c.lines_len = $('#source p').length; c.body_h = $('body').height(); c.header_h = $('div#header').height(); - c.missed_lines = $('td.text p.mis, td.text p.par'); // Build html - c.resize_scroll_markers(); + c.build_scroll_markers(); }; -coverage.resize_scroll_markers = function () { +coverage.build_scroll_markers = function () { var c = coverage, min_line_height = 3, max_line_height = 10, visible_window_h = $(window).height(); + c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'); $('#scroll_marker').remove(); // Don't build markers if the window has no scroll bar. if (c.body_h <= visible_window_h) { @@ -555,11 +555,16 @@ coverage.resize_scroll_markers = function () { var previous_line = -99, last_mark, - last_top; + last_top, + offsets = {}; - c.missed_lines.each(function () { - var line_top = Math.round($(this).offset().top * marker_scale), - id_name = $(this).attr('id'), + // Calculate line offsets outside loop to prevent relayouts + c.lines_to_mark.each(function() { + offsets[this.id] = $(this).offset().top; + }); + c.lines_to_mark.each(function () { + var id_name = $(this).attr('id'), + line_top = Math.round(offsets[id_name] * marker_scale), line_number = parseInt(id_name.substring(1, id_name.length)); if (line_number === previous_line + 1) { diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html index 777e73e81..c5323f34f 100644 --- a/doc/sample_html/index.html +++ b/doc/sample_html/index.html @@ -1,13 +1,9 @@ - - - Coverage report - @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,171 +34,131 @@

Coverage report: s m x - b p - c   change column sorting

-
- - - - - - - - - - - - + + + + + + - - - - - - - - - + + - + - - - + - - - - - - - - - + + + + + + - - + + - - - - - + + - - - + + - - - - - + + + - - + + - - - - + - + - - - - + - + - - - - - - + + + -
Module statements missing excludedbranches partialcoverage
Total135214132361098.49%151490172783439.90%
cogapp/__init__.py 2 0 00 0100.00%
cogapp/__main__.py 3 3 00 00.00%
cogapp/backward.py198226 04 2157.14%69.23%
cogapp/cogapp.py42734176498.84%48521512002849.34%
cogapp/makefiles.py282720 0314392.86%017.07%
cogapp/test_cogapp.py7040790549 66199.86%20030.00%
cogapp/test_makefiles.py557153 006 0100.00%23.38%
cogapp/test_whiteutils.py 6950 000 0100.00%27.54%
cogapp/whiteutils.py 455 0032198.70%34488.61%
-

No items found using the specified filter.

- - diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json index 6fd39e554..636975972 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,0,0,0,0]},"hash":"e00fa13d9d6f419ca35e732ea83c17fb"},"cogapp_test_makefiles_py":{"index":{"relative_filename":"cogapp/test_makefiles.py","html_filename":"cogapp_test_makefiles_py.html","nums":[1,55,0,0,6,0,0]},"hash":"db9f8cbf81e744216a3602f494704e64"},"cogapp_cogapp_py":{"index":{"relative_filename":"cogapp/cogapp.py","html_filename":"cogapp_cogapp_py.html","nums":[1,427,4,3,176,4,4]},"hash":"4755e7ad9cc02487511c151e968808d8"},"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,0,6,1,1]},"hash":"f8c759a0caca2f8e76270d8f1720e637"},"cogapp_makefiles_py":{"index":{"relative_filename":"cogapp/makefiles.py","html_filename":"cogapp_makefiles_py.html","nums":[1,28,3,0,14,3,3]},"hash":"1703ddbb839a9738578dc9bc8deab9b5"},"cogapp_whiteutils_py":{"index":{"relative_filename":"cogapp/whiteutils.py","html_filename":"cogapp_whiteutils_py.html","nums":[1,45,0,0,32,1,1]},"hash":"f0acf5699b528da74f67913134e4e21d"},"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.5","settings":"2bc9291143291213aaed835381c1a3fc","format":1} \ No newline at end of file +{"format":2,"version":"5.2.1","globals":"e6e162746063b971c76587652ff86e5f","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file diff --git a/doc/sample_html/style.css b/doc/sample_html/style.css index 86b820914..3e7f9b66b 100644 --- a/doc/sample_html/style.css +++ b/doc/sample_html/style.css @@ -1,375 +1,291 @@ +@charset "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 */ - -/* CSS styles for coverage.py. */ - -/* Page-wide styles */ -html, body, h1, h2, h3, p, table, td, th { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; - } - -/* Set baseline grid to 16 pt. */ -body { - font-family: georgia, serif; - font-size: 1em; - } - -html>body { - font-size: 16px; - } - -/* Set base font size to 12/16 */ -p { - font-size: .75em; /* 12/16 */ - line-height: 1.33333333em; /* 16/12 */ - } - -table { - border-collapse: collapse; - } -td { - vertical-align: top; -} -table tr.hidden { - display: none !important; - } - -p#no_rows { - display: none; - font-size: 1.2em; - } - -a.nav { - text-decoration: none; - color: inherit; - } -a.nav:hover { - text-decoration: underline; - color: inherit; - } - -/* Page structure */ -#header { - background: #f8f8f8; - width: 100%; - border-bottom: 1px solid #eee; - } - -#source { - padding: 1em; - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - } - -.indexfile #footer { - margin: 1em 3em; - } - -.pyfile #footer { - margin: 1em 1em; - } - -#footer .content { - padding: 0; - font-size: 85%; - font-family: verdana, sans-serif; - color: #666666; - font-style: italic; - } - -#index { - margin: 1em 0 0 3em; - } - -/* Header styles */ -#header .content { - padding: 1em 3em; - } - -h1 { - font-size: 1.25em; - display: inline-block; -} - -#filter_container { - display: inline-block; - float: right; - margin: 0 2em 0 0; -} -#filter_container input { - width: 10em; -} - -h2.stats { - margin-top: .5em; - font-size: 1em; -} -.stats span { - border: 1px solid; - padding: .1em .25em; - margin: 0 .1em; - cursor: pointer; - border-color: #999 #ccc #ccc #999; -} -.stats span.hide_run, .stats span.hide_exc, -.stats span.hide_mis, .stats span.hide_par, -.stats span.par.hide_run.hide_par { - border-color: #ccc #999 #999 #ccc; -} -.stats span.par.hide_run { - border-color: #999 #ccc #ccc #999; -} - -.stats span.run { - background: #ddffdd; -} -.stats span.exc { - background: #eeeeee; -} -.stats span.mis { - background: #ffdddd; -} -.stats span.hide_run { - background: #eeffee; -} -.stats span.hide_exc { - background: #f5f5f5; -} -.stats span.hide_mis { - background: #ffeeee; -} -.stats span.par { - background: #ffffaa; -} -.stats span.hide_par { - background: #ffffcc; -} - -/* Help panel */ -#keyboard_icon { - float: right; - margin: 5px; - cursor: pointer; -} - -.help_panel { - position: absolute; - background: #ffffcc; - padding: .5em; - border: 1px solid #883; - display: none; -} - -.indexfile .help_panel { - width: 20em; height: 4em; -} - -.pyfile .help_panel { - width: 16em; height: 8em; -} - -.help_panel .legend { - font-style: italic; - margin-bottom: 1em; -} - -#panel_icon { - float: right; - cursor: pointer; -} - -.keyhelp { - margin: .75em; -} - -.keyhelp .key { - border: 1px solid black; - border-color: #888 #333 #333 #888; - padding: .1em .35em; - font-family: monospace; - font-weight: bold; - background: #eee; -} - -/* Source file styles */ -.linenos p { - text-align: right; - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; - font-size: .625em; /* 10/16 */ - line-height: 1.6em; /* 16/10 */ - } -.linenos p.highlight { - background: #ffdd00; - } -.linenos p a { - text-decoration: none; - color: #999999; - } -.linenos p a:hover { - text-decoration: underline; - color: #999999; - } - -td.text { - width: 100%; - } -.text p { - margin: 0; - padding: 0 0 0 .5em; - border-left: 2px solid #ffffff; - white-space: pre; - position: relative; - } - -.text p.mis { - background: #ffdddd; - border-left: 2px solid #ff0000; - } -.text p.run, .text p.run.hide_par { - background: #ddffdd; - border-left: 2px solid #00ff00; - } -.text p.exc { - background: #eeeeee; - border-left: 2px solid #808080; - } -.text p.par, .text p.par.hide_run { - background: #ffffaa; - border-left: 2px solid #eeee99; - } -.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, -.text p.hide_run.hide_par { - background: inherit; - } - -.text span.annotate { - font-family: georgia; - color: #666; - float: right; - padding-right: .5em; - } -.text p.hide_par span.annotate { - display: none; - } -.text span.annotate.long { - display: none; - } -.text p:hover span.annotate.long { - display: block; - max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - width: 30em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; - } - -/* Syntax coloring */ -.text .com { - color: green; - font-style: italic; - line-height: 1px; - } -.text .key { - font-weight: bold; - line-height: 1px; - } -.text .str { - color: #000080; - } - -/* index styles */ -#index td, #index th { - text-align: right; - width: 5em; - padding: .25em .5em; - border-bottom: 1px solid #eee; - } -#index th { - font-style: italic; - color: #333; - border-bottom: 1px solid #ccc; - cursor: pointer; - } -#index th:hover { - background: #eee; - border-bottom: 1px solid #999; - } -#index td.left, #index th.left { - padding-left: 0; - } -#index td.right, #index th.right { - padding-right: 0; - } -#index th.headerSortDown, #index th.headerSortUp { - border-bottom: 1px solid #000; - white-space: nowrap; - background: #eee; - } -#index th.headerSortDown:after { - content: " ↓"; -} -#index th.headerSortUp:after { - content: " ↑"; -} -#index td.name, #index th.name { - text-align: left; - width: auto; - } -#index td.name a { - text-decoration: none; - color: #000; - } -#index tr.total, -#index tr.total_dynamic { - } -#index tr.total td, -#index tr.total_dynamic td { - font-weight: bold; - border-top: 1px solid #ccc; - border-bottom: none; - } -#index tr.file:hover { - background: #eeeeee; - } -#index tr.file:hover td.name { - text-decoration: underline; - color: #000; - } - -/* scroll marker styles */ -#scroll_marker { - position: fixed; - right: 0; - top: 0; - width: 16px; - height: 100%; - background: white; - border-left: 1px solid #eee; - } - -#scroll_marker .marker { - background: #eedddd; - position: absolute; - min-height: 3px; - width: 100%; - } +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.2em; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +#header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #header { background: black; } } + +@media (prefers-color-scheme: dark) { #header { border-color: #333; } } + +.indexfile #footer { margin: 1rem 3rem; } + +.pyfile #footer { margin: 1rem 1rem; } + +#footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3rem; } + +#header .content { padding: 1rem 3rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } + +#filter_container input:focus { border-color: #007acc; } + +h2.stats { margin-top: .5em; font-size: 1em; } + +.stats button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { .stats button { border-color: #444; } } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { .stats button.run { background: #373d29; } } + +.stats button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.run.show_run { background: #373d29; } } + +.stats button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { .stats button.mis { background: #4b1818; } } + +.stats button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.mis.show_mis { background: #4b1818; } } + +.stats button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { .stats button.exc { background: #333; } } + +.stats button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.exc.show_exc { background: #333; } } + +.stats button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { .stats button.par { background: #650; } } + +.stats button.par.show_par { background: #ffa; border: 2px solid #dddd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.par.show_par { background: #650; } } + +.help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#keyboard_icon { float: right; margin: 5px; cursor: pointer; } + +.help_panel { padding: .5em; border: 1px solid #883; } + +.help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile .help_panel { width: 20em; min-height: 4em; } + +.pyfile .help_panel { width: 16em; min-height: 8em; } + +#panel_icon { float: right; cursor: pointer; } + +.keyhelp { margin: .75em; } + +.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } + +#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n a { text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p.highlight .n { background: #ffdd00; } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6A9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str { color: #0451A5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { color: #9CDCFE; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #dddd00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #d5f7ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #aef; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #aef; border-radius: .25em; margin-right: 1.75em; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#source p .ctxs span { display: block; text-align: right; } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; } + +#index th { font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th.headerSortDown, #index th.headerSortUp { white-space: nowrap; background: #eee; } + +@media (prefers-color-scheme: dark) { #index th.headerSortDown, #index th.headerSortUp { background: #333; } } + +#index th.headerSortDown:after { content: " ↑"; } + +#index th.headerSortUp:after { content: " ↓"; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.file:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } + +#index tr.file:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/doc/sleepy.rst b/doc/sleepy.rst new file mode 100644 index 000000000..ceee6f392 --- /dev/null +++ b/doc/sleepy.rst @@ -0,0 +1,21 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _sleepy: + +============ +Sleepy Snake +============ + +Coverage.py's mascot is Sleepy Snake, drawn by Ben Batchelder. Ben's art can +be found on `Instagram`_ and at `artofbatch.com`_. Some details of Sleepy's +creation are on `Ned's blog`__. + +__ https://nedbatchelder.com/blog/201912/sleepy_snake.html + +.. image:: media/sleepy-snake-600.png + :alt: Sleepy Snake, cozy in his snake-shaped bed. + + +.. _Instagram: https://instagram.com/artofbatch +.. _artofbatch.com: https://artofbatch.com diff --git a/doc/source.rst b/doc/source.rst index 640fc431b..241c3d96c 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _source: @@ -7,9 +7,6 @@ Specifying source files ======================= -.. :history: 20100725T172000, new in 3.4 - - When coverage.py is running your program and measuring its execution, it needs to know what code to measure and what code not to. Measurement imposes a speed penalty, and the collected data must be stored in memory and then on disk. @@ -80,11 +77,11 @@ reported. Usually you want to see all the code that was measured, but if you are measuring a large project, you may want to get reports for just certain parts. -The report commands (``report``, ``html``, ``annotate``, and ``xml``) all take -optional ``modules`` arguments, and ``--include`` and ``--omit`` switches. The -``modules`` arguments specify particular modules to report on. The ``include`` -and ``omit`` values are lists of file name patterns, just as with the ``run`` -command. +The report commands (``report``, ``html``, ``json``, ``annotate``, and ``xml``) +all take optional ``modules`` arguments, and ``--include`` and ``--omit`` +switches. The ``modules`` arguments specify particular modules to report on. +The ``include`` and ``omit`` values are lists of file name patterns, just as +with the ``run`` command. Remember that the reporting commands can only report on the data that has been collected, so the data you're looking for may not be in the data available for diff --git a/doc/subprocess.rst b/doc/subprocess.rst index 7236b7ef1..c09d45f16 100644 --- a/doc/subprocess.rst +++ b/doc/subprocess.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _subprocess: @@ -7,10 +7,6 @@ Measuring sub-processes ======================= -.. :history: 20100224T201800, new for 3.3. -.. :history: 20100725T211700, updated for 3.4. - - Complex test suites may spawn sub-processes to run tests, either to run them in parallel, or because sub-process behavior is an important part of the system under test. Measuring coverage in those sub-processes can be tricky because you @@ -22,6 +18,16 @@ examines the ``COVERAGE_PROCESS_START`` environment variable, and if it is set, begins coverage measurement. The environment variable's value will be used as the name of the :ref:`configuration file ` to use. +.. note:: + The subprocess only sees options in the configuration file. Options set on + the command line will not be used in the subprocesses. + +.. note:: + If you have subprocesses because you are using :mod:`multiprocessing + `, the ``--concurrency=multiprocessing`` + command-line option should take care of everything for you. See + :ref:`cmd_run` for details. + When using this technique, be sure to set the parallel option to true so that multiple coverage.py runs will each write their data to a distinct file. diff --git a/doc/trouble.rst b/doc/trouble.rst index e3951218f..08f8a4d3c 100644 --- a/doc/trouble.rst +++ b/doc/trouble.rst @@ -1,5 +1,5 @@ .. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -.. For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt .. _trouble: @@ -7,10 +7,6 @@ Things that cause trouble ========================= -.. :history: 20121231T085200, brand new docs. -.. :history: 20150124T160800, remove obsolete stuff. - - Coverage.py works well, and I want it to properly measure any Python program, but there are some situations it can't cope with. This page details some known problems, with possible courses of action, and links to coverage.py bug reports @@ -53,21 +49,6 @@ coverage.py from working properly: .. _issue 43: https://bitbucket.org/ned/coveragepy/issues/43/coverage-measurement-fails-on-code -Things that require --timid ---------------------------- - -Some packages interfere with coverage measurement, but you might be able to -make it work by using the ``--timid`` command-line switch, or the ``[run] -timid=True`` configuration option. - -* `DecoratorTools`_, or any package which uses it, notably `TurboGears`_. - DecoratorTools fiddles with the trace function. You will need to use - ``--timid``. - -.. _DecoratorTools: https://pypi.python.org/pypi/DecoratorTools -.. _TurboGears: http://turbogears.org/ - - Still having trouble? --------------------- diff --git a/doc/whatsnew5x.rst b/doc/whatsnew5x.rst new file mode 100644 index 000000000..5f21a5d47 --- /dev/null +++ b/doc/whatsnew5x.rst @@ -0,0 +1,140 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _whatsnew5x: + +==================== +Major changes in 5.0 +==================== + +This is an overview of the changes in 5.0 since the last version of 4.5.x. This +is not a complete list of all changes. See the :ref:`complete change history +` for all the details. + + +Open Questions +-------------- + +- How to support SQL access to data? The database schema has to be convenient + and efficient for coverage.py's execution, which would naturally make it an + internal implementation detail. But the coverage data is now more complex, + and SQL access could be a powerful way to access it, pointing toward a public + guaranteed schema. What's the right balance? + + +Backward Incompatibilities +-------------------------- + +- Python 2.6, 3.3 and 3.4 are no longer supported. + +- The :class:`.CoverageData` interface is still public, but has changed. + +- The data file is now created earlier than it used to be. In some + circumstances, you may need to use ``parallel=true`` to avoid multiple + processes overwriting each others' data. + +- When constructing a :class:`coverage.Coverage` object, `data_file` can be + specified as None to prevent writing any data file at all. In previous + versions, an explicit `data_file=None` argument would use the default of + ".coverage". Fixes :github:`871`. + +- The ``[run] note`` setting has been deprecated. Using it will result in a + warning, and the note will not be written to the data file. The + corresponding :class:`.CoverageData` methods have been removed. + +- The deprecated `Reporter.file_reporters` property has been removed. + +- The reporting methods used to permanently apply their arguments to the + configuration of the Coverage object. Now they no longer do. The arguments + affect the operation of the method, but do not persist. + +- Many internal attributes and functions were changed. These were not part of + the public supported API. If your code used them, it might now stop working. + + +New Features +------------ + +- Coverage.py can now record the context in which each line was executed. The + contexts are stored in the data file and can be used to drill down into why a + particular line was run. Static contexts let you specify a label for an + entire coverage run, for example to separate coverage for different operating + systems or versions of Python. Dynamic contexts can change during a single + measurement run. This can be used to record the names of the tests that + executed each line. See :ref:`contexts` for full information. + +- Coverage's data storage has changed. In version 4.x, .coverage files were + basically JSON. Now, they are SQLite databases. The database schema is + documented (:ref:`dbschema`), but might still be in flux. + +- Data can now be "reported" in JSON format, for programmatic use, as requested + in :github:`720`. The new ``coverage json`` command writes raw and + summarized data to a JSON file. Thanks, Matt Bachmann. + +- Configuration can now be read from `TOML`_ files. This requires installing + coverage.py with the ``[toml]`` extra. The standard "pyproject.toml" file + will be read automatically if no other configuration file is found, with + settings in the ``[tool.coverage.]`` namespace. Thanks to Frazer McLean for + implementation and persistence. Finishes :github:`664`. + +- The HTML and textual reports now have a ``--skip-empty`` option that skips + files with no statements, notably ``__init__.py`` files. Thanks, Reya B. + +- You can specify the command line to run your program with the ``[run] + command_line`` configuration setting, as requested in :github:`695`. + +- An experimental ``[run] relative_files`` setting tells coverage to store + relative file names in the data file. This makes it easier to run tests in + one (or many) environments, and then report in another. It has not had much + real-world testing, so it may change in incompatible ways in the future. + +- Environment variable substitution in configuration files now supports two + syntaxes for controlling the behavior of undefined variables: if ``VARNAME`` + is not defined, ``${VARNAME?}`` will raise an error, and ``${VARNAME-default + value}`` will use "default value". + +- The location of the configuration file can now be specified with a + ``COVERAGE_RCFILE`` environment variable, as requested in `issue 650`_. + +- A new warning (``already-imported``) is issued if measurable files have + already been imported before coverage.py started measurement. See + :ref:`cmd_warnings` for more information. + +- Error handling during reporting has changed slightly. All reporting methods + now behave the same. The ``--ignore-errors`` option keeps errors from + stopping the reporting, but files that couldn't parse as Python will always + be reported as warnings. As with other warnings, you can suppress them with + the ``[run] disable_warnings`` configuration setting. + +- Added the classmethod :meth:`.Coverage.current` to get the latest started + Coverage instance. + + +.. _TOML: https://github.com/toml-lang/toml#readme +.. _issue 650: https://bitbucket.org/ned/coveragepy/issues/650/allow-setting-configuration-file-location + + +Bugs Fixed +---------- + +- The ``coverage run`` command has always adjusted the first entry in sys.path, + to properly emulate how Python runs your program. Now this adjustment is + skipped if sys.path[0] is already different than Python's default. This + fixes :github:`715`. + +- Python files run with ``-m`` now have ``__spec__`` defined properly. This + fixes :github:`745` (about not being able to run unittest tests that spawn + subprocesses), and :github:`838`, which described the problem directly. + +- Coverage will create directories as needed for the data file if they don't + exist, closing :github:`721`. + +- ``fail_under`` values more than 100 are reported as errors. Thanks to Mike + Fiedler for closing :github:`746`. + +- The "missing" values in the text output are now sorted by line number, so + that missing branches are reported near the other lines they affect. The + values used to show all missing lines, and then all missing branches. + +- Coverage.py no longer fails if the user program deletes its current + directory. Fixes :github:`806`. Thanks, Dan Hemberger. diff --git a/howto.txt b/howto.txt index 14c51916e..10a90232b 100644 --- a/howto.txt +++ b/howto.txt @@ -1,88 +1,103 @@ * Release checklist +- Check that the current virtualenv matches the current coverage branch. - Version number in coverage/version.py - version_info = (4, 0, 2, 'alpha', 1) - version_info = (4, 0, 2, 'beta', 1) - version_info = (4, 0, 2, 'candidate', 1) - version_info = (4, 0, 2, 'final', 0) + version_info = (4, 0, 2, "alpha", 1) + version_info = (4, 0, 2, "beta", 1) + version_info = (4, 0, 2, "candidate", 1) + version_info = (4, 0, 2, "final", 0) - Python version number in classifiers in setup.py - Copyright date in NOTICE.txt +- Update specific Python versions in appveyor.yml ("PYTHON_VERSION") - Update CHANGES.rst, including release date. + - don't forget the jump target - Update README.rst - "New in x.y:" + - Python versions supported - Update docs - - Version, date and python versions in doc/index.rst - - Version and copyright date in doc/conf.py + - Python versions in doc/index.rst + - Version of latest stable release in doc/index.rst + - Version, release, release_date 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 - coverage combine - coverage html - - IF BETA: - rm -f ~/coverage/trunk/doc/sample_html_beta/*.* - cp -r htmlcov/ ~/coverage/trunk/doc/sample_html_beta/ - - IF NOT BETA: - rm -f ~/coverage/trunk/doc/sample_html/*.* - cp -r htmlcov/ ~/coverage/trunk/doc/sample_html/ - cd ~/coverage/trunk - - IF NOT BETA: - check in the new sample html - - Build and publish docs: - - IF BETA: - $ make publishbeta - - ELSE: - $ make publish +- Generate new sample_html to get the latest, incl footer version number: + $ make clean + $ pip install -e . + $ cd ~/cog/trunk + $ rm -rf htmlcov + $ coverage run --branch --source=cogapp -m pytest -k CogTestsInMemory; coverage combine; coverage html + - IF PRE-RELEASE: + $ rm -f ~/coverage/trunk/doc/sample_html_beta/*.* + $ cp -r htmlcov/ ~/coverage/trunk/doc/sample_html_beta/ + - IF NOT PRE-RELEASE: + $ rm -f ~/coverage/trunk/doc/sample_html/*.* + $ cp -r htmlcov/ ~/coverage/trunk/doc/sample_html/ + cd ~/coverage/trunk + - IF NOT PRE-RELEASE: + check in the new sample html +- Done with changes to source files, check them in. + $ git push +- Build and publish docs: + - IF PRE-RELEASE: + $ make publishbeta + - ELSE: + $ make publish - Kits: - Start fresh: - - $ make clean + $ make sterile - Source kit and wheels: - - $ make kit wheel + $ make kit wheel - Linux wheels: - - $ make manylinux + $ make kit_linux - Windows kits - wait for over an hour for Appveyor to build kits. - https://ci.appveyor.com/project/nedbat/coveragepy - - $ make download_appveyor + $ make download_appveyor - examine the dist directory, and remove anything that looks malformed. -- Update PyPi: + - check the dist directory: + $ python -m twine check dist/* + - test the pypi upload: + $ make test_upload +- Update PyPI: - upload kits: - - $ make kit_upload - - Visit https://pypi.python.org/pypi?:action=pkg_edit&name=coverage : - - show/hide the proper versions. + $ make kit_upload - Tag the tree - - hg tag -m "Coverage 3.0.1" coverage-3.0.1 + $ git tag coverage-3.0.1 + $ git push --tags - Bump version: - coverage/version.py - increment version number - - IF NOT BETA: - - set to alpha-0 if just released + - IF NOT PRE-RELEASE: + - set to alpha-0 if just released. - CHANGES.rst - add an "Unreleased" section to the top. -- push hg changes -- Update nedbatchelder.com - - Blog post? + $ git push +- Update Tidelift: + $ make tidelift_relnotes +- Update GitHub releases: + $ make github_releases - Update readthedocs - - visit https://readthedocs.org/projects/coverage/versions/ + - @ 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/versions/ - - change the default version to the new version -- Update bitbucket: - - Issue tracker should get new version number in picker. - # Note: don't delete old version numbers: it marks changes on the tickets - # with that number. -- Announce on coveragepy-announce@googlegroups.com . -- Announce on TIP. + - readthedocs won't find the tag until a commit is made on master. + - keep just the latest version of each x.y release, make the rest inactive. + - IF NOT PRE-RELEASE: + - update git "stable" branch to point to latest release + $ git branch -f stable + $ git push --all + - @ https://readthedocs.org/projects/coverage/builds/ + - wait for the new tag build to finish successfully. + - @ https://readthedocs.org/dashboard/coverage/advanced/ + - change the default version to the new version +- Visit the fixed issues on GitHub and mention the version it was fixed in. + - "This is now released as part of [coverage 5.2](https://pypi.org/project/coverage/5.2)." + - make a milestone for the next release and move open issues into it. +- Announce: + - twitter @coveragepy + - nedbatchelder.com blog post? + - testing-in-python mailing list? * Testing @@ -93,7 +108,7 @@ - $ tox - Testing on Linux: - - $ docker run -it --init --rm -v `pwd`:/io quay.io/pypa/manylinux1_x86_64 /io/ci/manylinux.sh test + - $ make test_linux - For complete coverage testing: diff --git a/igor.py b/igor.py index 43ce3303a..3d51e5769 100644 --- a/igor.py +++ b/igor.py @@ -1,6 +1,6 @@ # 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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Helper for building, testing, and linting coverage.py. @@ -74,8 +74,10 @@ def label_for_tracer(tracer): def should_skip(tracer): """Is there a reason to skip these tests?""" if tracer == "py": + # $set_env.py: COVERAGE_NO_PYTRACER - Don't run the tests under the Python tracer. skipper = os.environ.get("COVERAGE_NO_PYTRACER") else: + # $set_env.py: COVERAGE_NO_CTRACER - Don't run the tests under the C tracer. skipper = os.environ.get("COVERAGE_NO_CTRACER") if skipper: @@ -88,10 +90,23 @@ def should_skip(tracer): return msg +def make_env_id(tracer): + """An environment id that will keep all the test runs distinct.""" + impl = platform.python_implementation().lower() + version = "%s%s" % sys.version_info[:2] + if '__pypy__' in sys.builtin_module_names: + version += "_%s%s" % sys.pypy_version_info[:2] + env_id = "%s%s_%s" % (impl, version, tracer) + return env_id + + def run_tests(tracer, *runner_args): """The actual running of tests.""" if 'COVERAGE_TESTING' not in os.environ: os.environ['COVERAGE_TESTING'] = "True" + # $set_env.py: COVERAGE_ENV_ID - Use environment-specific test directories. + if 'COVERAGE_ENV_ID' in os.environ: + os.environ['COVERAGE_ENV_ID'] = make_env_id(tracer) print_banner(label_for_tracer(tracer)) return pytest.main(list(runner_args)) @@ -106,27 +121,19 @@ def run_tests_with_coverage(tracer, *runner_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? + # There's an entry in "make clean" to get rid of this 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") - # Make names for the data files that keep all the test runs distinct. - impl = platform.python_implementation().lower() - version = "%s%s" % sys.version_info[:2] - if '__pypy__' in sys.builtin_module_names: - version += "_%s%s" % sys.pypy_version_info[:2] - suffix = "%s%s_%s_%s" % (impl, version, tracer, platform.platform()) - + suffix = "%s_%s" % (make_env_id(tracer), platform.platform()) os.environ['COVERAGE_METAFILE'] = os.path.abspath(".metacov."+suffix) import coverage - cov = coverage.Coverage(config_file="metacov.ini", data_suffix=False) - # Cheap trick: the coverage.py code itself is excluded from measurement, - # but if we clobber the cover_prefix in the coverage object, we can defeat - # the self-detection. - cov.cover_prefix = "Please measure coverage.py!" + cov = coverage.Coverage(config_file="metacov.ini") cov._warn_unimported_source = False + cov._warn_preimported_source = False cov.start() try: @@ -167,7 +174,8 @@ def do_combine_html(): cov.load() cov.combine() cov.save() - cov.html_report() + show_contexts = bool(os.environ.get('COVERAGE_CONTEXT')) + cov.html_report(show_contexts=show_contexts) cov.xml_report() @@ -180,7 +188,7 @@ def do_test_with_tracer(tracer, *runner_args): return None os.environ["COVERAGE_TEST_TRACER"] = tracer - if os.environ.get("COVERAGE_COVERAGE", ""): + if os.environ.get("COVERAGE_COVERAGE", "no") == "yes": return run_tests_with_coverage(tracer, *runner_args) else: return run_tests(tracer, *runner_args) @@ -210,18 +218,21 @@ def do_zip_mods(): (u'cp1252', u'“hi”'), ] for encoding, text in details: - filename = 'encoded_{0}.py'.format(encoding) + filename = 'encoded_{}.py'.format(encoding) ords = [ord(c) for c in text] source_text = source.format(encoding=encoding, text=text, ords=ords) zf.writestr(filename, source_text.encode(encoding)) zf.close() + zf = zipfile.ZipFile("tests/covmain.zip", "w") + zf.write("coverage/__main__.py", "__main__.py") + zf.close() + def do_install_egg(): """Install the egg1 egg for tests.""" # I am pretty certain there are easier ways to install eggs... - # pylint: disable=import-error,no-name-in-module cur_dir = os.getcwd() os.chdir("tests/eggsrc") with ignore_warnings(): @@ -242,6 +253,8 @@ def do_check_eol(): '.tox*', '*.egg-info', '_build', + '_spell', + 'tmp', ] checked = set() @@ -289,15 +302,13 @@ def check_files(root, patterns, **kwargs): check_files("coverage", ["*.py"]) check_files("coverage/ctracer", ["*.c", "*.h"]) - check_files("coverage/htmlfiles", ["*.html", "*.css", "*.js"]) - check_file("tests/farm/html/src/bom.py", crlf=False) + check_files("coverage/htmlfiles", ["*.html", "*.scss", "*.css", "*.js"]) check_files("tests", ["*.py"]) check_files("tests", ["*,cover"], trail_white=False) check_files("tests/js", ["*.js", "*.html"]) check_file("setup.py") check_file("igor.py") check_file("Makefile") - check_file(".hgignore") check_file(".travis.yml") check_files(".", ["*.rst", "*.txt"]) check_files(".", ["*.pip"]) @@ -346,7 +357,9 @@ def analyze_args(function): getargspec = inspect.getfullargspec except AttributeError: getargspec = inspect.getargspec - argspec = getargspec(function) + with ignore_warnings(): + # DeprecationWarning: Use inspect.signature() instead of inspect.getfullargspec() + argspec = getargspec(function) return bool(argspec[1]), len(argspec[0]) diff --git a/lab/README.txt b/lab/README.txt new file mode 100644 index 000000000..3263667a4 --- /dev/null +++ b/lab/README.txt @@ -0,0 +1,4 @@ +The lab directory is not part of the installed coverage.py code. These programs +are tools I have used while diagnosing problems, investigating functionality, +and so on. They are not guaranteed to work, or to be suitable for any given +purpose. If you find them useful, enjoy! diff --git a/lab/branches.py b/lab/branches.py index d1908d0fa..c2b838dda 100644 --- a/lab/branches.py +++ b/lab/branches.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Demonstrate some issues with coverage.py branch testing. @@ -13,11 +13,11 @@ def my_function(x): i = 0 while True: - print "In while True" + print("In while True") if i > 0: break i += 1 - print "Left the True loop" + print("Left the True loop") # Notice that "while 1" also has this problem. Even though the compiler # knows there's no computation at the top of the loop, it's still expressed @@ -25,11 +25,11 @@ def my_function(x): i = 0 while 1: - print "In while 1" + print("In while 1") if i > 0: break i += 1 - print "Left the 1 loop" + print("Left the 1 loop") # Coverage.py lets developers exclude lines that they know will not be # executed. So far, the branch coverage doesn't use all that information @@ -40,9 +40,9 @@ def my_function(x): if x < 1000: # This branch is always taken - print "x is reasonable" + print("x is reasonable") else: # pragma: nocover - print "this never happens" + print("this never happens") # try-except structures are complex branches. An except clause with a # type is a three-way branch: there could be no exception, there could be @@ -57,9 +57,9 @@ def my_function(x): if y % 2: raise ValueError("y is odd!") except ValueError: - print "y must have been odd" - print "done with y" - print "done with 1, 2" + print("y must have been odd") + print("done with y") + print("done with 1, 2") # Another except clause, but this time all three cases are executed. No # partial lines are shown: @@ -71,11 +71,11 @@ def my_function(x): if y == 0: raise Exception("zero!") except ValueError: - print "y must have been odd" + print("y must have been odd") except: - print "y is something else" - print "done with y" - print "done with 0, 1, 2" + print("y is something else") + print("done with y") + print("done with 0, 1, 2") my_function(1) diff --git a/lab/compare_times.sh b/lab/compare_times.sh new file mode 100755 index 000000000..c5f20bd95 --- /dev/null +++ b/lab/compare_times.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +# A suggestion about how to get less hyperfine output: +# https://github.com/sharkdp/hyperfine/issues/223 +HYPERFINE='hyperfine -w 1 -s basic -r 10' + +cat > sourcefile1.py << EOF +import random + +def get_random_number(): + return random.randint(5, 20) +EOF + +cat > test_file1.py << EOF +import pytest +import sourcefile1 + +tests = tuple(f'test{i}' for i in range(1000)) + +@pytest.mark.parametrize("input_str", tests) +def test_speed(input_str): + print(input_str) + number = sourcefile1.get_random_number() + assert number <= 20 + assert number >= 5 +EOF + +rm -f .coveragerc + +$HYPERFINE 'python -m pytest test_file1.py' + +echo "Coverage 4.5.4" +pip install -q coverage==4.5.4 +$HYPERFINE 'python -m coverage run -m pytest test_file1.py' +$HYPERFINE 'python -m coverage run --branch -m pytest test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= --cov-branch test_file1.py' + +echo "Coverage 5.0a8, no contexts" +pip install -q coverage==5.0a8 +$HYPERFINE 'python -m coverage run -m pytest test_file1.py' +$HYPERFINE 'python -m coverage run --branch -m pytest test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= test_file1.py' +$HYPERFINE 'python -m pytest --cov=. --cov-report= --cov-branch test_file1.py' + +echo "Coverage 5.0a8, with test contexts" +cat > .coveragerc <= (3, 0): - setup_args.update(dict( - use_2to3=False, - )) - def main(): """Actually invoke setup() with the arguments we built above.""" diff --git a/tests/__init__.py b/tests/__init__.py index 1ff1e1be6..38f2ff8b0 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,4 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Automated tests. Run with pytest.""" diff --git a/tests/backtest.py b/tests/backtest.py deleted file mode 100644 index 827e891f9..000000000 --- a/tests/backtest.py +++ /dev/null @@ -1,19 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -"""Add things to old Pythons so I can pretend they are newer, for tests.""" - -# pylint: disable=redefined-builtin -# (Redefining built-in blah) -# The whole point of this file is to redefine built-ins, so shut up about it. - - -# No more execfile in Py3 -try: - execfile = execfile -except NameError: - def execfile(filename, globs): - """A Python 3 implementation of execfile.""" - with open(filename) as fobj: - code = fobj.read() - exec(compile(code, filename, 'exec'), globs) diff --git a/tests/conftest.py b/tests/conftest.py index a0c21a84a..82a6b0f2f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """ Pytest auto configuration. @@ -7,12 +7,72 @@ This module is run automatically by pytest, to define and enable fixtures. """ -import pytest +import os +import sys import warnings +import pytest + +from coverage import env + + +# Pytest can take additional options: +# $set_env.py: PYTEST_ADDOPTS - Extra arguments to pytest. @pytest.fixture(autouse=True) def set_warnings(): """Enable DeprecationWarnings during all tests.""" warnings.simplefilter("default") warnings.simplefilter("once", DeprecationWarning) + + # A warning to suppress: + # setuptools/py33compat.py:54: DeprecationWarning: The value of convert_charrefs will become + # True in 3.5. You are encouraged to set the value explicitly. + # unescape = getattr(html, 'unescape', html_parser.HTMLParser().unescape) + # How come this warning is successfully suppressed here, but not in setup.cfg?? + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message="The value of convert_charrefs will become True in 3.5.", + ) + warnings.filterwarnings( + "ignore", + category=DeprecationWarning, + message=".* instead of inspect.getfullargspec", + ) + if env.PYPY3: + # pypy3 warns about unclosed files a lot. + warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning) + + +@pytest.fixture(autouse=True) +def reset_sys_path(): + """Clean up sys.path changes around every test.""" + sys_path = list(sys.path) + yield + sys.path[:] = sys_path + + +@pytest.fixture(autouse=True) +def fix_xdist_sys_path(): + """Prevent xdist from polluting the Python path. + + We run tests that care a lot about the contents of sys.path. Pytest-xdist + changes sys.path, so running with xdist, vs without xdist, sets sys.path + differently. With xdist, sys.path[1] is an empty string, without xdist, + it's the virtualenv bin directory. We don't want the empty string, so + clobber that entry. + + See: https://github.com/pytest-dev/pytest-xdist/issues/376 + + """ + if os.environ.get('PYTEST_XDIST_WORKER', ''): + # We are running in an xdist worker. + if sys.path[1] == '': + # xdist has set sys.path[1] to ''. Clobber it. + del sys.path[1] + # Also, don't let it sneak stuff in via PYTHONPATH. + try: + del os.environ['PYTHONPATH'] + except KeyError: + pass diff --git a/tests/coveragetest.py b/tests/coveragetest.py index 29d634c41..58cfb3dc6 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -1,18 +1,21 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Base test case class for coverage.py testing.""" import contextlib import datetime import functools +import glob import os +import os.path import random import re import shlex import sys import types +import pytest from unittest_mixins import ( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, DelayedAssertionMixin, @@ -23,9 +26,9 @@ 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 from coverage.misc import StopEverything +from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs from tests.helpers import run_command, SuperModuleCleaner @@ -39,24 +42,23 @@ def convert_skip_exceptions(method): """A decorator for test methods to convert StopEverything to SkipTest.""" @functools.wraps(method) - def wrapper(*args, **kwargs): - """Run the test method, and convert exceptions.""" + def _wrapper(*args, **kwargs): try: result = method(*args, **kwargs) except StopEverything: raise unittest.SkipTest("StopEverything!") return result - return wrapper + return _wrapper class SkipConvertingMetaclass(type): """Decorate all test methods to convert StopEverything to SkipTest.""" - def __new__(mcs, name, bases, attrs): + def __new__(cls, 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) + return super(SkipConvertingMetaclass, cls).__new__(cls, name, bases, attrs) CoverageTestMethodsMixin = SkipConvertingMetaclass('CoverageTestMethodsMixin', (), {}) @@ -80,6 +82,15 @@ class CoverageTest( # Let stderr go to stderr, pytest will capture it for us. show_stderr = True + # Temp dirs go to $TMPDIR/coverage_test/* + temp_dir_prefix = "coverage_test/" + if os.getenv('COVERAGE_ENV_ID'): + temp_dir_prefix += "{}/".format(os.getenv('COVERAGE_ENV_ID')) + + # Keep the temp directories if the env says to. + # $set_env.py: COVERAGE_KEEP_TMP - Keep the temp directories made by tests. + keep_temp_dir = bool(int(os.getenv("COVERAGE_KEEP_TMP", "0"))) + def setUp(self): super(CoverageTest, self).setUp() @@ -90,12 +101,6 @@ def setUp(self): self.last_command_output = None self.last_module_name = None - if _TEST_NAME_FILE: # pragma: debugging - with open(_TEST_NAME_FILE, "w") as f: - f.write("%s_%s" % ( - self.__class__.__name__, self._testMethodName, - )) - def clean_local_file_imports(self): """Clean up the results of calls to `import_local_file`. @@ -129,49 +134,11 @@ def get_module_name(self): self.last_module_name = 'coverage_test_' + str(random.random())[2:] return self.last_module_name - # Map chars to numbers for arcz_to_arcs - _arcz_map = {'.': -1} - _arcz_map.update(dict((c, ord(c) - ord('0')) for c in '123456789')) - _arcz_map.update(dict( - (c, 10 + ord(c) - ord('A')) for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - )) - - def arcz_to_arcs(self, arcz): - """Convert a compact textual representation of arcs to a list of pairs. - - The text has space-separated pairs of letters. Period is -1, 1-9 are - 1-9, A-Z are 10 through 36. The resulting list is sorted regardless of - the order of the input pairs. - - ".1 12 2." --> [(-1,1), (1,2), (2,-1)] - - Minus signs can be included in the pairs: - - "-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)] - - """ - arcs = [] - for pair in arcz.split(): - asgn = bsgn = 1 - if len(pair) == 2: - a, b = pair - else: - assert len(pair) == 3 - if pair[0] == '-': - _, a, b = pair - asgn = -1 - else: - assert pair[1] == '-' - a, _, b = pair - bsgn = -1 - arcs.append((asgn * self._arcz_map[a], bsgn * self._arcz_map[b])) - return sorted(arcs) - - def assert_equal_args(self, a1, a2, msg=None): + def assert_equal_arcs(self, a1, a2, msg=None): """Assert that the arc lists `a1` and `a2` are equal.""" # Make them into multi-line strings so we can see what's going wrong. - s1 = "\n".join(repr(a) for a in a1) + "\n" - s2 = "\n".join(repr(a) for a in a2) + "\n" + s1 = arcs_to_arcz_repr(a1) + s2 = arcs_to_arcz_repr(a2) self.assertMultiLineEqual(s1, s2, msg) def check_coverage( @@ -205,11 +172,11 @@ def check_coverage( self.make_file(modname + ".py", text) if arcs is None and arcz is not None: - arcs = self.arcz_to_arcs(arcz) + arcs = arcz_to_arcs(arcz) if arcs_missing is None: - arcs_missing = self.arcz_to_arcs(arcz_missing) + arcs_missing = arcz_to_arcs(arcz_missing) if arcs_unpredicted is None: - arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted) + arcs_unpredicted = arcz_to_arcs(arcz_unpredicted) # Start up coverage.py. cov = coverage.Coverage(branch=True) @@ -253,19 +220,19 @@ def check_coverage( if arcs is not None: with self.delayed_assertions(): - self.assert_equal_args( - analysis.arc_possibilities(), arcs, - "Possible arcs differ: minus is actual, plus is expected" + self.assert_equal_arcs( + arcs, analysis.arc_possibilities(), + "Possible arcs differ: minus is expected, plus is actual" ) - self.assert_equal_args( - analysis.arcs_missing(), arcs_missing, - "Missing arcs differ: minus is actual, plus is expected" + self.assert_equal_arcs( + arcs_missing, analysis.arcs_missing(), + "Missing arcs differ: minus is expected, plus is actual" ) - self.assert_equal_args( - analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ: minus is actual, plus is expected" + self.assert_equal_arcs( + arcs_unpredicted, analysis.arcs_unpredicted(), + "Unpredicted arcs differ: minus is expected, plus is actual" ) if report: @@ -294,8 +261,9 @@ def assert_warnings(self, cov, warnings, not_warnings=()): """ saved_warnings = [] - def capture_warning(msg, slug=None): + def capture_warning(msg, slug=None, once=False): # pylint: disable=unused-argument """A fake implementation of Coverage._warn, to capture warnings.""" + # NOTE: we don't implement `once`. if slug: msg = "%s (%s)" % (msg, slug) saved_warnings.append(msg) @@ -305,7 +273,7 @@ def capture_warning(msg, slug=None): try: yield - except: + except: # pylint: disable=try-except-raise raise else: if warnings: @@ -347,6 +315,13 @@ def assert_doesnt_exist(self, fname): msg = "File %r shouldn't exist" % fname self.assertTrue(not os.path.exists(fname), msg) + def assert_file_count(self, pattern, count): + """Assert that there are `count` files matching `pattern`.""" + files = sorted(glob.glob(pattern)) + msg = "There should be {} files matching {!r}, but there are these: {}" + msg = msg.format(count, pattern, files) + self.assertEqual(len(files), count, msg) + def assert_starts_with(self, s, prefix, msg=None): """Assert that `s` starts with `prefix`.""" if not s.startswith(prefix): @@ -355,12 +330,10 @@ def assert_starts_with(self, s, prefix, msg=None): def assert_recent_datetime(self, dt, seconds=10, msg=None): """Assert that `dt` marks a time at most `seconds` seconds ago.""" age = datetime.datetime.now() - dt - # Python2.6 doesn't have total_seconds :( - self.assertEqual(age.days, 0, msg) - self.assertGreaterEqual(age.seconds, 0, msg) - self.assertLessEqual(age.seconds, seconds, msg) + self.assertGreaterEqual(age.total_seconds(), 0, msg) + self.assertLessEqual(age.total_seconds(), seconds, msg) - def command_line(self, args, ret=OK, _covpkg=None): + def command_line(self, args, ret=OK): """Run `args` through the command line. Use this when you want to run the full coverage machinery, but in the @@ -372,7 +345,7 @@ def command_line(self, args, ret=OK, _covpkg=None): Returns None. """ - ret_actual = command_line(args, _covpkg=_covpkg) + ret_actual = command_line(args) self.assertEqual(ret_actual, ret) coverage_command = "coverage" @@ -407,7 +380,7 @@ def run_command_status(self, cmd): Python interpreter. * "coverage" is replaced with the command name for the main - Coverage.py program. + coverage.py program. Returns a pair: the process' exit status and its stdout/stderr text, which are also stored as `self.last_command_status` and @@ -438,8 +411,8 @@ def run_command_status(self, cmd): # modules. command_words = "jython -m coverage".split() else: - # The invocation requests the Coverage.py program. Substitute the - # actual Coverage.py main command name. + # The invocation requests the coverage.py program. Substitute the + # actual coverage.py main command name. command_words = [self.coverage_command] else: @@ -461,13 +434,6 @@ def run_command_status(self, cmd): pypath += testmods + os.pathsep + zipfile self.set_environ(pythonpath_name, pypath) - # There are environment variables that we set when we are running the - # coverage test suite under coverage. We don't want these environment - # variables to leak into subprocesses we start for a test. Delete them - # before running the subprocess command. - self.del_environ("COVERAGE_COVERAGE") - self.del_environ("COVERAGE_PROCESS_START") - 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 @@ -501,6 +467,15 @@ def last_line_squeezed(self, report): """Return the last line of `report` with the spaces squeezed down.""" return self.squeezed_lines(report)[-1] + def get_measured_filenames(self, coverage_data): + """Get paths to measured files. + + Returns a dict of {filename: absolute path to file} + for given CoverageData. + """ + return {os.path.basename(filename): filename + for filename in coverage_data.measured_files()} + class UsingModulesMixin(object): """A mixin for importing modules from tests/modules and tests/moremodules.""" @@ -513,14 +488,17 @@ def setUp(self): sys.path.append(self.nice_file(TESTS_DIR, 'moremodules')) -def command_line(args, **kwargs): +def command_line(args): """Run `args` through the CoverageScript command line. - `kwargs` are the keyword arguments to the CoverageScript constructor. - Returns the return code from CoverageScript.command_line. """ - script = CoverageScript(**kwargs) + script = CoverageScript() ret = script.command_line(shlex.split(args)) return ret + + +def xfail(condition, reason): + """A decorator to mark as test as expected to fail.""" + return pytest.mark.xfail(condition, reason=reason, strict=True) diff --git a/tests/covmodzip1.py b/tests/covmodzip1.py index cab642814..c4867e177 100644 --- a/tests/covmodzip1.py +++ b/tests/covmodzip1.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 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # Module-level docstrings are counted differently in different versions of Python, # so don't add one here. -# pylint: disable=missing-docstring +# pylint: disable=missing-module-docstring # covmodzip.py: for putting into a zip file. j = 1 diff --git a/tests/eggsrc/egg1/egg1.py b/tests/eggsrc/egg1/egg1.py index 72600808d..939386e3f 100644 --- a/tests/eggsrc/egg1/egg1.py +++ b/tests/eggsrc/egg1/egg1.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt # My egg file! diff --git a/tests/eggsrc/setup.py b/tests/eggsrc/setup.py index c935798db..26a0b650f 100644 --- a/tests/eggsrc/setup.py +++ b/tests/eggsrc/setup.py @@ -1,5 +1,5 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt from setuptools import setup diff --git a/tests/farm/annotate/annotate_dir.py b/tests/farm/annotate/annotate_dir.py deleted file mode 100644 index 3bb2dbe74..000000000 --- a/tests/farm/annotate/annotate_dir.py +++ /dev/null @@ -1,10 +0,0 @@ -# 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", "run") -run(""" - coverage run multi.py - coverage annotate -d out_anno_dir - """, rundir="run") -compare("run/out_anno_dir", "gold_anno_dir", "*,cover", left_extra=True) -clean("run") diff --git a/tests/farm/annotate/gold_anno_dir/a_a.py,cover b/tests/farm/annotate/gold_anno_dir/a_a.py,cover deleted file mode 100644 index 4729cfbb4..000000000 --- a/tests/farm/annotate/gold_anno_dir/a_a.py,cover +++ /dev/null @@ -1,8 +0,0 @@ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -> def a(x): -> if x == 1: -> print("x is 1") -! else: -! print("x is not 1") diff --git a/tests/farm/annotate/gold_anno_dir/b_b.py,cover b/tests/farm/annotate/gold_anno_dir/b_b.py,cover deleted file mode 100644 index 228715f05..000000000 --- a/tests/farm/annotate/gold_anno_dir/b_b.py,cover +++ /dev/null @@ -1,6 +0,0 @@ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -> def b(x): -> msg = "x is %s" % x -> print(msg) diff --git a/tests/farm/annotate/gold_anno_dir/multi.py,cover b/tests/farm/annotate/gold_anno_dir/multi.py,cover deleted file mode 100644 index 90a13c915..000000000 --- a/tests/farm/annotate/gold_anno_dir/multi.py,cover +++ /dev/null @@ -1,8 +0,0 @@ - # 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 a.a -> import b.b - -> a.a.a(1) -> b.b.b(2) diff --git a/tests/farm/annotate/gold_encodings/utf8.py,cover b/tests/farm/annotate/gold_encodings/utf8.py,cover deleted file mode 100644 index 3ef31e0fa..000000000 --- a/tests/farm/annotate/gold_encodings/utf8.py,cover +++ /dev/null @@ -1,7 +0,0 @@ - # -*- 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 - - # This comment has an accent: é - -> print("spam eggs") diff --git a/tests/farm/annotate/gold_multi/a/a.py,cover b/tests/farm/annotate/gold_multi/a/a.py,cover deleted file mode 100644 index e5e972264..000000000 --- a/tests/farm/annotate/gold_multi/a/a.py,cover +++ /dev/null @@ -1,8 +0,0 @@ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -> def a(x): -> if x == 1: -> print "x is 1" -! else: -! print "x is not 1" diff --git a/tests/farm/annotate/gold_multi/b/b.py,cover b/tests/farm/annotate/gold_multi/b/b.py,cover deleted file mode 100644 index 26b25548a..000000000 --- a/tests/farm/annotate/gold_multi/b/b.py,cover +++ /dev/null @@ -1,5 +0,0 @@ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -> def b(x): -> print "x is %s" % x diff --git a/tests/farm/annotate/gold_multi/multi.py,cover b/tests/farm/annotate/gold_multi/multi.py,cover deleted file mode 100644 index 90a13c915..000000000 --- a/tests/farm/annotate/gold_multi/multi.py,cover +++ /dev/null @@ -1,8 +0,0 @@ - # 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 a.a -> import b.b - -> a.a.a(1) -> b.b.b(2) diff --git a/tests/farm/annotate/run.py b/tests/farm/annotate/run.py deleted file mode 100644 index 33e5f6711..000000000 --- a/tests/farm/annotate/run.py +++ /dev/null @@ -1,10 +0,0 @@ -# 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") -run(""" - coverage run white.py - coverage annotate white.py - """, rundir="out") -compare("out", "gold", "*,cover") -clean("out") diff --git a/tests/farm/annotate/run_encodings.py b/tests/farm/annotate/run_encodings.py deleted file mode 100644 index 46d8c643e..000000000 --- a/tests/farm/annotate/run_encodings.py +++ /dev/null @@ -1,10 +0,0 @@ -# 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_encodings") -run(""" - coverage run utf8.py - coverage annotate utf8.py - """, rundir="out_encodings") -compare("out_encodings", "gold_encodings", "*,cover") -clean("out_encodings") diff --git a/tests/farm/annotate/run_multi.py b/tests/farm/annotate/run_multi.py deleted file mode 100644 index 4646293e9..000000000 --- a/tests/farm/annotate/run_multi.py +++ /dev/null @@ -1,10 +0,0 @@ -# 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_multi") -run(""" - coverage run multi.py - coverage annotate - """, rundir="out_multi") -compare("out_multi", "gold_multi", "*,cover") -clean("out_multi") diff --git a/tests/farm/annotate/src/a/__init__.py b/tests/farm/annotate/src/a/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/farm/annotate/src/a/a.py b/tests/farm/annotate/src/a/a.py deleted file mode 100644 index e3e6631d7..000000000 --- a/tests/farm/annotate/src/a/a.py +++ /dev/null @@ -1,8 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -def a(x): - if x == 1: - print("x is 1") - else: - print("x is not 1") diff --git a/tests/farm/annotate/src/b/__init__.py b/tests/farm/annotate/src/b/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/farm/annotate/src/b/b.py b/tests/farm/annotate/src/b/b.py deleted file mode 100644 index b31d8c956..000000000 --- a/tests/farm/annotate/src/b/b.py +++ /dev/null @@ -1,6 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -def b(x): - msg = "x is %s" % x - print(msg) diff --git a/tests/farm/annotate/src/multi.py b/tests/farm/annotate/src/multi.py deleted file mode 100644 index bf8cfd5fb..000000000 --- a/tests/farm/annotate/src/multi.py +++ /dev/null @@ -1,8 +0,0 @@ -# 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 a.a -import b.b - -a.a.a(1) -b.b.b(2) diff --git a/tests/farm/annotate/src/utf8.py b/tests/farm/annotate/src/utf8.py deleted file mode 100644 index fd43b2ab0..000000000 --- a/tests/farm/annotate/src/utf8.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- 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 - -# This comment has an accent: é - -print("spam eggs") diff --git a/tests/farm/annotate/src/white.py b/tests/farm/annotate/src/white.py deleted file mode 100644 index 21e8a6275..000000000 --- a/tests/farm/annotate/src/white.py +++ /dev/null @@ -1,36 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A test case sent to me by Steve White - -def f(self): - if self==1: - pass - elif self.m('fred'): - pass - elif (g==1) and (b==2): - pass - elif self.m('fred')==True: - pass - elif ((g==1) and (b==2))==True: - pass - else: - pass - -def g(x): - if x == 1: - a = 1 - else: - a = 2 - -g(1) - -def h(x): - if 0: #pragma: no cover - pass - if x == 1: - a = 1 - else: - a = 2 - -h(2) diff --git a/tests/farm/html/gold_a/a_py.html b/tests/farm/html/gold_a/a_py.html deleted file mode 100644 index 00ab529d6..000000000 --- a/tests/farm/html/gold_a/a_py.html +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - Coverage for a.py: 67% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

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 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

if 1 < 2: 

-

    # Needed a < to look at HTML entities. 

-

    a = 3 

-

else: 

-

    a = 4 

- -
-
- - - - - diff --git a/tests/farm/html/gold_b_branch/b_py.html b/tests/farm/html/gold_b_branch/b_py.html deleted file mode 100644 index 4a8765005..000000000 --- a/tests/farm/html/gold_b_branch/b_py.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - - Coverage for b.py: 70% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

-

22

-

23

-

24

-

25

-

26

-

27

-

28

-

29

-

30

-

31

-

32

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

def one(x): 

-

    # This will be a branch that misses the else. 

-

11    if x < 2: 

-

        a = 3 

-

    else: 

-

        a = 4 

-

 

-

one(1) 

-

 

-

def two(x): 

-

    # A missed else that branches to "exit" 

-

exit    if x: 

-

        a = 5 

-

 

-

two(1) 

-

 

-

def three(): 

-

    try: 

-

        # This if has two branches, *neither* one taken. 

-

26   28        if name_error_this_variable_doesnt_exist: 

-

            a = 1 

-

        else: 

-

            a = 2 

-

    except: 

-

        pass 

-

 

-

three() 

- -
-
- - - - - diff --git a/tests/farm/html/gold_bom/bom_py.html b/tests/farm/html/gold_bom/bom_py.html deleted file mode 100644 index 127f2f451..000000000 --- a/tests/farm/html/gold_bom/bom_py.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - Coverage for bom: 71% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

- -
-

# A python source file in utf-8, with BOM 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

 

-

import sys 

-

 

-

if sys.version_info >= (3, 0): 

-

    assert len(math) == 18 

-

    assert len(math.encode('utf-8')) == 21 

-

else: 

-

    assert len(math) == 21 

-

    assert len(math.decode('utf-8')) == 18 

- -
-
- - - - - diff --git a/tests/farm/html/gold_isolatin1/isolatin1_py.html b/tests/farm/html/gold_isolatin1/isolatin1_py.html deleted file mode 100644 index bad0d78d0..000000000 --- a/tests/farm/html/gold_isolatin1/isolatin1_py.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - Coverage for isolatin1.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

- -
-

# -*- coding: iso8859-1 -*- 

-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

# A Python source file in another encoding. 

-

 

-

math = "3×4 = 12, ÷2 = 6±0" 

-

assert len(math) == 18 

- -
-
- - - - - diff --git a/tests/farm/html/gold_omit_1/main_py.html b/tests/farm/html/gold_omit_1/main_py.html deleted file mode 100644 index d33dfedc8..000000000 --- a/tests/farm/html/gold_omit_1/main_py.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - Coverage for main.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

- -
-

# 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 m1 

-

import m2 

-

import m3 

-

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

- -
-
- - - - - diff --git a/tests/farm/html/gold_omit_2/main_py.html b/tests/farm/html/gold_omit_2/main_py.html deleted file mode 100644 index d33dfedc8..000000000 --- a/tests/farm/html/gold_omit_2/main_py.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - Coverage for main.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

- -
-

# 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 m1 

-

import m2 

-

import m3 

-

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

- -
-
- - - - - diff --git a/tests/farm/html/gold_omit_3/main_py.html b/tests/farm/html/gold_omit_3/main_py.html deleted file mode 100644 index d33dfedc8..000000000 --- a/tests/farm/html/gold_omit_3/main_py.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - Coverage for main.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

- -
-

# 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 m1 

-

import m2 

-

import m3 

-

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

- -
-
- - - - - diff --git a/tests/farm/html/gold_omit_4/main_py.html b/tests/farm/html/gold_omit_4/main_py.html deleted file mode 100644 index d33dfedc8..000000000 --- a/tests/farm/html/gold_omit_4/main_py.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - Coverage for main.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

- -
-

# 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 m1 

-

import m2 

-

import m3 

-

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

- -
-
- - - - - diff --git a/tests/farm/html/gold_omit_5/main_py.html b/tests/farm/html/gold_omit_5/main_py.html deleted file mode 100644 index d33dfedc8..000000000 --- a/tests/farm/html/gold_omit_5/main_py.html +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - Coverage for main.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

- -
-

# 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 m1 

-

import m2 

-

import m3 

-

 

-

a = 5 

-

b = 6 

-

 

-

assert m1.m1a == 1 

-

assert m2.m2a == 1 

-

assert m3.m3a == 1 

- -
-
- - - - - diff --git a/tests/farm/html/gold_other/blah_blah_other_py.html b/tests/farm/html/gold_other/blah_blah_other_py.html deleted file mode 100644 index 54861c23c..000000000 --- a/tests/farm/html/gold_other/blah_blah_other_py.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - Coverage for /Users/ned/coverage/trunk/tests/farm/html/othersrc/other.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

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 

-

 

-

# A file in another directory.  We're checking that it ends up in the 

-

# HTML report. 

-

 

-

print("This is the other src!") 

- -
-
- - - - - diff --git a/tests/farm/html/gold_other/here_py.html b/tests/farm/html/gold_other/here_py.html deleted file mode 100644 index 82aa70a2f..000000000 --- a/tests/farm/html/gold_other/here_py.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - Coverage for here.py: 75% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

import other 

-

 

-

if 1 < 2: 

-

    h = 3 

-

else: 

-

    h = 4 

- -
-
- - - - - diff --git a/tests/farm/html/gold_partial/partial_py.html b/tests/farm/html/gold_partial/partial_py.html deleted file mode 100644 index 796153f1f..000000000 --- a/tests/farm/html/gold_partial/partial_py.html +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - Coverage for partial.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

10

-

11

-

12

-

13

-

14

-

15

-

16

-

17

-

18

-

19

-

20

-

21

- -
-

# 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 

-

 

-

a = 3 

-

 

-

while True: 

-

    break 

-

 

-

while 1: 

-

    break 

-

 

-

while a:        # pragma: no branch 

-

    break 

-

 

-

if 0: 

-

    never_happen() 

-

 

-

if 1: 

-

    a = 13 

- -
-
- - - - - diff --git a/tests/farm/html/gold_styled/a_py.html b/tests/farm/html/gold_styled/a_py.html deleted file mode 100644 index c8221753b..000000000 --- a/tests/farm/html/gold_styled/a_py.html +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - Coverage for a.py: 67% - - - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

-

9

-

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 

-

 

-

# A test file for HTML reporting by coverage.py. 

-

 

-

if 1 < 2: 

-

    # Needed a < to look at HTML entities. 

-

    a = 3 

-

else: 

-

    a = 4 

- -
-
- - - - - diff --git a/tests/farm/html/gold_styled/style.css b/tests/farm/html/gold_styled/style.css deleted file mode 100644 index c3767eef6..000000000 --- a/tests/farm/html/gold_styled/style.css +++ /dev/null @@ -1,366 +0,0 @@ -/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ -/* For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt */ - -/* CSS styles for coverage.py. */ -/* Page-wide styles */ -html, body, h1, h2, h3, p, table, td, th { - margin: 0; - padding: 0; - border: 0; - outline: 0; - font-weight: inherit; - font-style: inherit; - font-size: 100%; - font-family: inherit; - vertical-align: baseline; - } - -/* Set baseline grid to 16 pt. */ -body { - font-family: georgia, serif; - font-size: 1em; - } - -html>body { - font-size: 16px; - } - -/* Set base font size to 12/16 */ -p { - font-size: .75em; /* 12/16 */ - line-height: 1.33333333em; /* 16/12 */ - } - -table { - border-collapse: collapse; - } -td { - vertical-align: top; -} -table tr.hidden { - display: none !important; - } - -p#no_rows { - display: none; - font-size: 1.2em; - } - -a.nav { - text-decoration: none; - color: inherit; - } -a.nav:hover { - text-decoration: underline; - color: inherit; - } - -/* Page structure */ -#header { - background: #f8f8f8; - width: 100%; - border-bottom: 1px solid #eee; - } - -#source { - padding: 1em; - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - } - -.indexfile #footer { - margin: 1em 3em; - } - -.pyfile #footer { - margin: 1em 1em; - } - -#footer .content { - padding: 0; - font-size: 85%; - font-family: verdana, sans-serif; - color: #666666; - font-style: italic; - } - -#index { - margin: 1em 0 0 3em; - } - -/* Header styles */ -#header .content { - padding: 1em 3em; - } - -h1 { - font-size: 1.25em; - display: inline-block; -} - -#filter_container { - display: inline-block; - float: right; - margin: 0 2em 0 0; -} -#filter_container input { - width: 10em; -} - -h2.stats { - margin-top: .5em; - font-size: 1em; -} -.stats span { - border: 1px solid; - padding: .1em .25em; - margin: 0 .1em; - cursor: pointer; - border-color: #999 #ccc #ccc #999; -} -.stats span.hide_run, .stats span.hide_exc, -.stats span.hide_mis, .stats span.hide_par, -.stats span.par.hide_run.hide_par { - border-color: #ccc #999 #999 #ccc; -} -.stats span.par.hide_run { - border-color: #999 #ccc #ccc #999; -} - -.stats span.run { - background: #ddffdd; -} -.stats span.exc { - background: #eeeeee; -} -.stats span.mis { - background: #ffdddd; -} -.stats span.hide_run { - background: #eeffee; -} -.stats span.hide_exc { - background: #f5f5f5; -} -.stats span.hide_mis { - background: #ffeeee; -} -.stats span.par { - background: #ffffaa; -} -.stats span.hide_par { - background: #ffffcc; -} - -/* Help panel */ -#keyboard_icon { - float: right; - margin: 5px; - cursor: pointer; -} - -.help_panel { - position: absolute; - background: #ffffcc; - padding: .5em; - border: 1px solid #883; - display: none; -} - -.indexfile .help_panel { - width: 20em; height: 4em; -} - -.pyfile .help_panel { - width: 16em; height: 8em; -} - -.help_panel .legend { - font-style: italic; - margin-bottom: 1em; -} - -#panel_icon { - float: right; - cursor: pointer; -} - -.keyhelp { - margin: .75em; -} - -.keyhelp .key { - border: 1px solid black; - border-color: #888 #333 #333 #888; - padding: .1em .35em; - font-family: monospace; - font-weight: bold; - background: #eee; -} - -/* Source file styles */ -.linenos p { - text-align: right; - margin: 0; - padding: 0 .5em; - color: #999999; - font-family: verdana, sans-serif; - font-size: .625em; /* 10/16 */ - line-height: 1.6em; /* 16/10 */ - } -.linenos p.highlight { - background: #ffdd00; - } -.linenos p a { - text-decoration: none; - color: #999999; - } -.linenos p a:hover { - text-decoration: underline; - color: #999999; - } - -td.text { - width: 100%; - } -.text p { - margin: 0; - padding: 0 0 0 .5em; - border-left: 2px solid #ffffff; - white-space: pre; - position: relative; - } - -.text p.mis { - background: #ffdddd; - border-left: 2px solid #ff0000; - } -.text p.run, .text p.run.hide_par { - background: #ddffdd; - border-left: 2px solid #00ff00; - } -.text p.exc { - background: #eeeeee; - border-left: 2px solid #808080; - } -.text p.par, .text p.par.hide_run { - background: #ffffaa; - border-left: 2px solid #eeee99; - } -.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par, -.text p.hide_run.hide_par { - background: inherit; - } - -.text span.annotate { - font-family: georgia; - color: #666; - float: right; - padding-right: .5em; - } -.text p.hide_par span.annotate { - display: none; - } -.text span.annotate.long { - display: none; - } -.text p:hover span.annotate.long { - display: block; - max-width: 50%; - white-space: normal; - float: right; - position: absolute; - top: 1.75em; - right: 1em; - width: 30em; - height: auto; - color: #333; - background: #ffffcc; - border: 1px solid #888; - padding: .25em .5em; - z-index: 999; - border-radius: .2em; - box-shadow: #cccccc .2em .2em .2em; - } - -/* Syntax coloring */ -.text .com { - color: green; - font-style: italic; - line-height: 1px; - } -.text .key { - font-weight: bold; - line-height: 1px; - } -.text .str { - color: #000080; - } - -/* index styles */ -#index td, #index th { - text-align: right; - width: 5em; - padding: .25em .5em; - border-bottom: 1px solid #eee; - } -#index th { - font-style: italic; - color: #333; - border-bottom: 1px solid #ccc; - cursor: pointer; - } -#index th:hover { - background: #eee; - border-bottom: 1px solid #999; - } -#index td.left, #index th.left { - padding-left: 0; - } -#index td.right, #index th.right { - padding-right: 0; - } -#index th.headerSortDown, #index th.headerSortUp { - border-bottom: 1px solid #000; - } -#index td.name, #index th.name { - text-align: left; - width: auto; - } -#index td.name a { - text-decoration: none; - color: #000; - } -#index tr.total, -#index tr.total_dynamic { - } -#index tr.total td, -#index tr.total_dynamic td { - font-weight: bold; - border-top: 1px solid #ccc; - border-bottom: none; - } -#index tr.file:hover { - background: #eeeeee; - } -#index tr.file:hover td.name { - text-decoration: underline; - color: #000; - } - -/* scroll marker styles */ -#scroll_marker { - position: fixed; - right: 0; - top: 0; - width: 16px; - height: 100%; - background: white; - border-left: 1px solid #eee; - } - -#scroll_marker .marker { - background: #ffdddd; - position: absolute; - min-height: 3px; - width: 100%; - } diff --git a/tests/farm/html/gold_unicode/unicode_py.html b/tests/farm/html/gold_unicode/unicode_py.html deleted file mode 100644 index 83b0f382d..000000000 --- a/tests/farm/html/gold_unicode/unicode_py.html +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - Coverage for unicode.py: 100% - - - - - - - - - - - - -
- Hide keyboard shortcuts -

Hot-keys on this page

-
-

- r - m - x - p   toggle line displays -

-

- j - k   next/prev highlighted chunk -

-

- 0   (zero) top of page -

-

- 1   (one) first highlighted chunk -

-
-
- -
- - - - - -
-

1

-

2

-

3

-

4

-

5

-

6

-

7

-

8

- -
-

# -*- 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 

-

 

-

# A Python source file with exotic characters. 

-

 

-

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

-

surrogate = "db40,dd00: x󠄀" 

- -
-
- - - - - diff --git a/tests/farm/html/othersrc/other.py b/tests/farm/html/othersrc/other.py deleted file mode 100644 index bf0304d29..000000000 --- a/tests/farm/html/othersrc/other.py +++ /dev/null @@ -1,7 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A file in another directory. We're checking that it ends up in the -# HTML report. - -print("This is the other src!") diff --git a/tests/farm/html/src/a.py b/tests/farm/html/src/a.py deleted file mode 100644 index 85764e211..000000000 --- a/tests/farm/html/src/a.py +++ /dev/null @@ -1,10 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A test file for HTML reporting by coverage.py. - -if 1 < 2: - # Needed a < to look at HTML entities. - a = 3 -else: - a = 4 diff --git a/tests/farm/html/src/b.py b/tests/farm/html/src/b.py deleted file mode 100644 index cb673c229..000000000 --- a/tests/farm/html/src/b.py +++ /dev/null @@ -1,32 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A test file for HTML reporting by coverage.py. - -def one(x): - # This will be a branch that misses the else. - if x < 2: - a = 3 - else: - a = 4 - -one(1) - -def two(x): - # A missed else that branches to "exit" - if x: - a = 5 - -two(1) - -def three(): - try: - # This if has two branches, *neither* one taken. - if name_error_this_variable_doesnt_exist: - a = 1 - else: - a = 2 - except: - pass - -three() diff --git a/tests/farm/html/src/bom.py b/tests/farm/html/src/bom.py deleted file mode 100644 index 21d26ca27..000000000 --- a/tests/farm/html/src/bom.py +++ /dev/null @@ -1,14 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A Python source file in utf-8, with BOM. -math = "3×4 = 12, ÷2 = 6±0" - -import sys - -if sys.version_info >= (3, 0): - assert len(math) == 18 - assert len(math.encode('utf-8')) == 21 -else: - assert len(math) == 21 - assert len(math.decode('utf-8')) == 18 diff --git a/tests/farm/html/src/extra.css b/tests/farm/html/src/extra.css deleted file mode 100644 index 46c41fcd3..000000000 --- a/tests/farm/html/src/extra.css +++ /dev/null @@ -1 +0,0 @@ -/* Doesn't matter what goes in here, it gets copied. */ diff --git a/tests/farm/html/src/here.py b/tests/farm/html/src/here.py deleted file mode 100644 index fee9960db..000000000 --- a/tests/farm/html/src/here.py +++ /dev/null @@ -1,11 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A test file for HTML reporting by coverage.py. - -import other - -if 1 < 2: - h = 3 -else: - h = 4 diff --git a/tests/farm/html/src/isolatin1.py b/tests/farm/html/src/isolatin1.py deleted file mode 100644 index 55a6f7def..000000000 --- a/tests/farm/html/src/isolatin1.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- coding: iso8859-1 -*- -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A Python source file in another encoding. - -math = "34 = 12, 2 = 60" -assert len(math) == 18 diff --git a/tests/farm/html/src/m1.py b/tests/farm/html/src/m1.py deleted file mode 100644 index 524fb0aac..000000000 --- a/tests/farm/html/src/m1.py +++ /dev/null @@ -1,5 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -m1a = 1 -m1b = 2 diff --git a/tests/farm/html/src/m2.py b/tests/farm/html/src/m2.py deleted file mode 100644 index 2d13bfe23..000000000 --- a/tests/farm/html/src/m2.py +++ /dev/null @@ -1,5 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -m2a = 1 -m2b = 2 diff --git a/tests/farm/html/src/m3.py b/tests/farm/html/src/m3.py deleted file mode 100644 index 96e8b992a..000000000 --- a/tests/farm/html/src/m3.py +++ /dev/null @@ -1,5 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -m3a = 1 -m3b = 2 diff --git a/tests/farm/html/src/main.py b/tests/farm/html/src/main.py deleted file mode 100644 index 238d0b58b..000000000 --- a/tests/farm/html/src/main.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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 m1 -import m2 -import m3 - -a = 5 -b = 6 - -assert m1.m1a == 1 -assert m2.m2a == 1 -assert m3.m3a == 1 diff --git a/tests/farm/html/src/omit4.ini b/tests/farm/html/src/omit4.ini deleted file mode 100644 index b792e7037..000000000 --- a/tests/farm/html/src/omit4.ini +++ /dev/null @@ -1,5 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -[report] -omit = m2.py diff --git a/tests/farm/html/src/omit5.ini b/tests/farm/html/src/omit5.ini deleted file mode 100644 index 3b6add295..000000000 --- a/tests/farm/html/src/omit5.ini +++ /dev/null @@ -1,11 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -[report] -omit = - fooey - gooey, m[23]*, kablooey - helloworld - -[html] -directory = ../out/omit_5 diff --git a/tests/farm/html/src/partial.ini b/tests/farm/html/src/partial.ini deleted file mode 100644 index cdb241b5c..000000000 --- a/tests/farm/html/src/partial.ini +++ /dev/null @@ -1,9 +0,0 @@ -# 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 deleted file mode 100644 index 0f8fbe3cc..000000000 --- a/tests/farm/html/src/partial.py +++ /dev/null @@ -1,24 +0,0 @@ -# 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 and excluded lines - -a = 6 - -while True: - break - -while 1: - break - -while a: # pragma: no branch - break - -if 0: - never_happen() - -if 1: - a = 21 - -if a == 23: - raise AssertionError("Can't") diff --git a/tests/farm/html/src/run_a_xml_2.ini b/tests/farm/html/src/run_a_xml_2.ini deleted file mode 100644 index f632bd097..000000000 --- a/tests/farm/html/src/run_a_xml_2.ini +++ /dev/null @@ -1,6 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# Put all the XML output in xml_2 -[xml] -output = ../out/xml_2/coverage.xml diff --git a/tests/farm/html/src/tabbed.py b/tests/farm/html/src/tabbed.py deleted file mode 100644 index e897e9fa3..000000000 --- a/tests/farm/html/src/tabbed.py +++ /dev/null @@ -1,10 +0,0 @@ -# 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 should have tabs. -x = 1 -if x: - a = "Tabbed" # Aligned comments - if x: # look nice - b = "No spaces" # when they - c = "Done" # line up. diff --git a/tests/farm/html/src/unicode.py b/tests/farm/html/src/unicode.py deleted file mode 100644 index 37c5533af..000000000 --- a/tests/farm/html/src/unicode.py +++ /dev/null @@ -1,8 +0,0 @@ -# -*- 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 - -# A Python source file with exotic characters. - -upside_down = "ʎd˙ǝbɐɹǝʌoɔ" -surrogate = "db40,dd00: x󠄀" diff --git a/tests/farm/html/src/y.py b/tests/farm/html/src/y.py deleted file mode 100644 index a50bb629d..000000000 --- a/tests/farm/html/src/y.py +++ /dev/null @@ -1,12 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# A test file for XML reporting by coverage.py. - -def choice(x): - if x < 2: - return 3 - else: - return 4 - -assert choice(1) == 3 diff --git a/tests/farm/run/run_chdir.py b/tests/farm/run/run_chdir.py deleted file mode 100644 index 1da4e9a32..000000000 --- a/tests/farm/run/run_chdir.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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_chdir") -run(""" - coverage run chdir.py - coverage report - """, rundir="out_chdir", outfile="stdout.txt") -contains("out_chdir/stdout.txt", - "Line One", - "Line Two", - "chdir" - ) -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 deleted file mode 100644 index 0370cf848..000000000 --- a/tests/farm/run/run_timid.py +++ /dev/null @@ -1,43 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# Test that the --timid command line argument properly swaps the tracer -# function for a simpler one. -# -# This is complicated by the fact that the tests are run twice for each -# version: once with a compiled C-based trace function, and once without -# it, to also test the Python trace function. So this test has to examine -# an environment variable set in igor.py to know whether to expect to see -# the C trace function or not. - -import os - -# When meta-coverage testing, this test doesn't work, because it finds -# coverage.py's own trace function. -if os.environ.get('COVERAGE_COVERAGE', ''): - skip("Can't test timid during coverage measurement.") - -copy("src", "out_timid") -run(""" - python showtrace.py none - coverage run showtrace.py regular - coverage run --timid showtrace.py timid - """, rundir="out_timid", outfile="showtraceout.txt") - -# When running without coverage, no trace function -# When running timidly, the trace function is always Python. -contains("out_timid/showtraceout.txt", - "none None", - "timid PyTracer", - ) - -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_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_timid/showtraceout.txt", "regular PyTracer") - -clean("out_timid") diff --git a/tests/farm/run/run_xxx.py b/tests/farm/run/run_xxx.py deleted file mode 100644 index 1db5b0d0c..000000000 --- a/tests/farm/run/run_xxx.py +++ /dev/null @@ -1,15 +0,0 @@ -# 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_xxx") -run(""" - coverage run xxx - coverage report - """, 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_xxx/stdout.txt", "No such file or directory") -clean("out_xxx") diff --git a/tests/farm/run/src/chdir.py b/tests/farm/run/src/chdir.py deleted file mode 100644 index 35cfcc811..000000000 --- a/tests/farm/run/src/chdir.py +++ /dev/null @@ -1,7 +0,0 @@ -# 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 os -print("Line One") -os.chdir("subdir") -print("Line Two") diff --git a/tests/farm/run/src/showtrace.py b/tests/farm/run/src/showtrace.py deleted file mode 100644 index 3a2750a6f..000000000 --- a/tests/farm/run/src/showtrace.py +++ /dev/null @@ -1,26 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - -# Show the current frame's trace function, so that we can test what the -# command-line options do to the trace function used. - -import sys - -# Show what the trace function is. If a C-based function is used, then f_trace -# may be None. -trace_fn = sys._getframe(0).f_trace -if trace_fn is None: - trace_name = "None" -else: - # Get the name of the tracer class. Py3k has a different way to get it. - try: - trace_name = trace_fn.im_class.__name__ - except AttributeError: - try: - trace_name = trace_fn.__self__.__class__.__name__ - except AttributeError: - # A C-based function could also manifest as an f_trace value - # which doesn't have im_class or __self__. - trace_name = trace_fn.__class__.__name__ - -print("%s %s" % (sys.argv[1], trace_name)) diff --git a/tests/farm/run/src/subdir/placeholder b/tests/farm/run/src/subdir/placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/farm/run/src/xxx b/tests/farm/run/src/xxx deleted file mode 100644 index 864da4576..000000000 --- a/tests/farm/run/src/xxx +++ /dev/null @@ -1,11 +0,0 @@ -# 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 is a python file though it doesn't look like it, like a main script. -a = b = c = d = 0 -a = 3 -b = 4 -if not b: - c = 6 -d = 7 -print("xxx: %r %r %r %r" % (a, b, c, d)) diff --git a/tests/gold/README.rst b/tests/gold/README.rst new file mode 100644 index 000000000..aec00c71b --- /dev/null +++ b/tests/gold/README.rst @@ -0,0 +1,28 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +Gold files +========== + +These are files used in comparisons for some of the tests. Code to support +these comparisons is in tests/goldtest.py. + +If gold tests are failing, it can useful to set the COVERAGE_KEEP_TMP +environment variable. If set, the test working directories at +$TMPDIR/coverage_test are kept after the tests are run, so that you can +manually inspect the differences. + +Do this to clean the output directories and run only the failed tests while +keeping the output:: + + rm -rf $TMPDIR/coverage_test + COVERAGE_KEEP_TMP=1 tox -e py37 -- --lf + +The saved HTML files in the html directories can't be viewed properly without +the supporting CSS and Javascript files. But we don't want to save copies of +those files in every subdirectory. There's a Makefile in the html directory +for working with the saved copies of the support files. + +If the output files are correct, you can update the gold files with "make +update-gold". If there are version-specific gold files (for example, +bom/2/\*), you'll need to update them manually. diff --git a/tests/farm/annotate/gold_anno_dir/a___init__.py,cover b/tests/gold/annotate/anno_dir/a___init__.py,cover similarity index 100% rename from tests/farm/annotate/gold_anno_dir/a___init__.py,cover rename to tests/gold/annotate/anno_dir/a___init__.py,cover diff --git a/tests/gold/annotate/anno_dir/a_a.py,cover b/tests/gold/annotate/anno_dir/a_a.py,cover new file mode 100644 index 000000000..d0ff3c0c6 --- /dev/null +++ b/tests/gold/annotate/anno_dir/a_a.py,cover @@ -0,0 +1,5 @@ +> def a(x): +> if x == 1: +> print("x is 1") +! else: +! print("x is not 1") diff --git a/tests/farm/annotate/gold_anno_dir/b___init__.py,cover b/tests/gold/annotate/anno_dir/b___init__.py,cover similarity index 100% rename from tests/farm/annotate/gold_anno_dir/b___init__.py,cover rename to tests/gold/annotate/anno_dir/b___init__.py,cover diff --git a/tests/gold/annotate/anno_dir/b_b.py,cover b/tests/gold/annotate/anno_dir/b_b.py,cover new file mode 100644 index 000000000..90d076f14 --- /dev/null +++ b/tests/gold/annotate/anno_dir/b_b.py,cover @@ -0,0 +1,3 @@ +> def b(x): +> msg = "x is %s" % x +> print(msg) diff --git a/tests/gold/annotate/anno_dir/multi.py,cover b/tests/gold/annotate/anno_dir/multi.py,cover new file mode 100644 index 000000000..2a5c59ce2 --- /dev/null +++ b/tests/gold/annotate/anno_dir/multi.py,cover @@ -0,0 +1,5 @@ +> import a.a +> import b.b + +> a.a.a(1) +> b.b.b(2) diff --git a/tests/farm/annotate/gold/white.py,cover b/tests/gold/annotate/annotate/white.py,cover similarity index 76% rename from tests/farm/annotate/gold/white.py,cover rename to tests/gold/annotate/annotate/white.py,cover index fc1632267..36b0b993d 100644 --- a/tests/farm/annotate/gold/white.py,cover +++ b/tests/gold/annotate/annotate/white.py,cover @@ -1,6 +1,3 @@ - # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 - # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt - # A test case sent to me by Steve White > def f(self): diff --git a/tests/gold/annotate/encodings/utf8.py,cover b/tests/gold/annotate/encodings/utf8.py,cover new file mode 100644 index 000000000..6dbf7e0d9 --- /dev/null +++ b/tests/gold/annotate/encodings/utf8.py,cover @@ -0,0 +1,4 @@ + # -*- coding: utf-8 -*- + # This comment has an accent: é + +> print("spam eggs") diff --git a/tests/farm/annotate/gold_multi/a/__init__.py,cover b/tests/gold/annotate/multi/a/__init__.py,cover similarity index 100% rename from tests/farm/annotate/gold_multi/a/__init__.py,cover rename to tests/gold/annotate/multi/a/__init__.py,cover diff --git a/tests/gold/annotate/multi/a/a.py,cover b/tests/gold/annotate/multi/a/a.py,cover new file mode 100644 index 000000000..fb3f5435d --- /dev/null +++ b/tests/gold/annotate/multi/a/a.py,cover @@ -0,0 +1,5 @@ +> def a(x): +> if x == 1: +> print "x is 1" +! else: +! print "x is not 1" diff --git a/tests/farm/annotate/gold_multi/b/__init__.py,cover b/tests/gold/annotate/multi/b/__init__.py,cover similarity index 100% rename from tests/farm/annotate/gold_multi/b/__init__.py,cover rename to tests/gold/annotate/multi/b/__init__.py,cover diff --git a/tests/gold/annotate/multi/b/b.py,cover b/tests/gold/annotate/multi/b/b.py,cover new file mode 100644 index 000000000..a3f5daec7 --- /dev/null +++ b/tests/gold/annotate/multi/b/b.py,cover @@ -0,0 +1,2 @@ +> def b(x): +> print "x is %s" % x diff --git a/tests/gold/annotate/multi/multi.py,cover b/tests/gold/annotate/multi/multi.py,cover new file mode 100644 index 000000000..2a5c59ce2 --- /dev/null +++ b/tests/gold/annotate/multi/multi.py,cover @@ -0,0 +1,5 @@ +> import a.a +> import b.b + +> a.a.a(1) +> b.b.b(2) diff --git a/tests/gold/html/Makefile b/tests/gold/html/Makefile new file mode 100644 index 000000000..604ece7ac --- /dev/null +++ b/tests/gold/html/Makefile @@ -0,0 +1,27 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +help: + @echo "Available targets:" + @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' + +complete: ## Copy support files into directories so the HTML can be viewed properly. + @for sub in *; do \ + if [ -f "$$sub/index.html" ]; then \ + echo Copying into $$sub ; \ + cp -n support/* $$sub ; \ + fi ; \ + done ; \ + true # because the for loop exits with 1 for some reason. + +clean: ## Remove the effects of this Makefile. + git clean -fq . + +update-gold: ## Copy output files from latest tests to gold files. + @for sub in $$TMPDIR/coverage_test/*HtmlGoldTests*/out; do \ + rsync --verbose --existing --recursive $$sub/ . ; \ + done ; \ + true + +update-support: ## Copy latest support files here for posterity. + cp ../../../coverage/htmlfiles/*.{css,js,png} support diff --git a/tests/gold/html/a/a_py.html b/tests/gold/html/a/a_py.html new file mode 100644 index 000000000..af5d72a19 --- /dev/null +++ b/tests/gold/html/a/a_py.html @@ -0,0 +1,69 @@ + + + + + + Coverage for a.py: 67% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1if 1 < 2: 

+

2 # Needed a < to look at HTML entities. 

+

3 a = 3 

+

4else: 

+

5 a = 4 

+
+ + + diff --git a/tests/farm/html/gold_a/index.html b/tests/gold/html/a/index.html similarity index 90% rename from tests/farm/html/gold_a/index.html rename to tests/gold/html/a/index.html index 35507b6f6..3276f1d65 100644 --- a/tests/farm/html/gold_a/index.html +++ b/tests/gold/html/a/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,63 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 3 1 067%
a.py 3 1 067%
-

No items found using the specified filter.

- - diff --git a/tests/gold/html/b_branch/b_py.html b/tests/gold/html/b_branch/b_py.html new file mode 100644 index 000000000..ee28735e5 --- /dev/null +++ b/tests/gold/html/b_branch/b_py.html @@ -0,0 +1,92 @@ + + + + + + Coverage for b.py: 70% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1def one(x): 

+

2 # This will be a branch that misses the else. 

+

3 if x < 2: 3 ↛ 6line 3 didn't jump to line 6, because the condition on line 3 was never false

+

4 a = 3 

+

5 else: 

+

6 a = 4 

+

7 

+

8one(1) 

+

9 

+

10def two(x): 

+

11 # A missed else that branches to "exit" 

+

12 if x: 12 ↛ exitline 12 didn't return from function 'two', because the condition on line 12 was never false

+

13 a = 5 

+

14 

+

15two(1) 

+

16 

+

17def three(): 

+

18 try: 

+

19 # This if has two branches, *neither* one taken. 

+

20 if name_error_this_variable_doesnt_exist: 20 ↛ 21,   20 ↛ 232 missed branches: 1) line 20 didn't jump to line 21, because the condition on line 20 was never true, 2) line 20 didn't jump to line 23, because the condition on line 20 was never false

+

21 a = 1 

+

22 else: 

+

23 a = 2 

+

24 except: 

+

25 pass 

+

26 

+

27three() 

+
+ + + diff --git a/tests/farm/html/gold_b_branch/index.html b/tests/gold/html/b_branch/index.html similarity index 87% rename from tests/farm/html/gold_b_branch/index.html rename to tests/gold/html/b_branch/index.html index ebc3a1069..0dfc20cad 100644 --- a/tests/farm/html/gold_b_branch/index.html +++ b/tests/gold/html/b_branch/index.html @@ -4,9 +4,8 @@ Coverage report - - + @@ -15,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -39,74 +34,59 @@

Coverage report: s m x - b p - c   change column sorting

-
- - - - - - - - + - - -
Module statements missing excludedbranches partialcoverage
Total 17 3 06 470%
bb.py 17 3 06 470%
-

No items found using the specified filter.

- - diff --git a/tests/gold/html/bom/2/bom_py.html b/tests/gold/html/bom/2/bom_py.html new file mode 100644 index 000000000..a66988452 --- /dev/null +++ b/tests/gold/html/bom/2/bom_py.html @@ -0,0 +1,75 @@ + + + + + + Coverage for bom.py: 71% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1# A Python source file in utf-8, with BOM. 

+

2math = "3×4 = 12, ÷2 = 6±0" 

+

3 

+

4import sys 

+

5 

+

6if sys.version_info >= (3, 0): 

+

7 assert len(math) == 18 

+

8 assert len(math.encode('utf-8')) == 21 

+

9else: 

+

10 assert len(math) == 21 

+

11 assert len(math.decode('utf-8')) == 18 

+
+ + + diff --git a/tests/farm/html/gold_bom/index.html b/tests/gold/html/bom/2/index.html similarity index 87% rename from tests/farm/html/gold_bom/index.html rename to tests/gold/html/bom/2/index.html index 776967276..28abec0a6 100644 --- a/tests/farm/html/gold_bom/index.html +++ b/tests/gold/html/bom/2/index.html @@ -4,9 +4,8 @@ Coverage report - - + @@ -15,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -39,62 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - + - -
Module statements missing excludedcoverage
Total 7 2 071%
bombom.py 7 2 071%
-

No items found using the specified filter.

- - diff --git a/tests/gold/html/bom/bom_py.html b/tests/gold/html/bom/bom_py.html new file mode 100644 index 000000000..3b181c633 --- /dev/null +++ b/tests/gold/html/bom/bom_py.html @@ -0,0 +1,75 @@ + + + + + + Coverage for bom.py: 71% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1# A Python source file in utf-8, with BOM. 

+

2math = "3×4 = 12, ÷2 = 6±0" 

+

3 

+

4import sys 

+

5 

+

6if sys.version_info >= (3, 0): 

+

7 assert len(math) == 18 

+

8 assert len(math.encode('utf-8')) == 21 

+

9else: 

+

10 assert len(math) == 21 

+

11 assert len(math.decode('utf-8')) == 18 

+
+ + + diff --git a/tests/gold/html/bom/index.html b/tests/gold/html/bom/index.html new file mode 100644 index 000000000..0e56a99a6 --- /dev/null +++ b/tests/gold/html/bom/index.html @@ -0,0 +1,84 @@ + + + + + Coverage report + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ n + s + m + x + c   change column sorting +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total72071%
bom.py72071%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/tests/farm/html/gold_isolatin1/index.html b/tests/gold/html/isolatin1/index.html similarity index 90% rename from tests/farm/html/gold_isolatin1/index.html rename to tests/gold/html/isolatin1/index.html index ee49cc5e0..ec9c50b52 100644 --- a/tests/farm/html/gold_isolatin1/index.html +++ b/tests/gold/html/isolatin1/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,63 +34,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 2 0 0100%
isolatin1.py 2 0 0100%
-

No items found using the specified filter.

- - diff --git a/tests/gold/html/isolatin1/isolatin1_py.html b/tests/gold/html/isolatin1/isolatin1_py.html new file mode 100644 index 000000000..3dd8c8fd0 --- /dev/null +++ b/tests/gold/html/isolatin1/isolatin1_py.html @@ -0,0 +1,69 @@ + + + + + + Coverage for isolatin1.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1# -*- coding: iso8859-1 -*- 

+

2# A Python source file in another encoding. 

+

3 

+

4math = "3×4 = 12, ÷2 = 6±0" 

+

5assert len(math) == 18 

+
+ + + diff --git a/tests/farm/html/gold_omit_1/index.html b/tests/gold/html/omit_1/index.html similarity index 90% rename from tests/farm/html/gold_omit_1/index.html rename to tests/gold/html/omit_1/index.html index 404d6b6ad..a97add0f6 100644 --- a/tests/farm/html/gold_omit_1/index.html +++ b/tests/gold/html/omit_1/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,90 +34,72 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - - - - - -
Module statements missing excludedcoverage
Total 14 0 0100%
m1.py 2 0 0100%
m2.py 2 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - diff --git a/tests/farm/html/gold_omit_1/m1_py.html b/tests/gold/html/omit_1/m1_py.html similarity index 53% rename from tests/farm/html/gold_omit_1/m1_py.html rename to tests/gold/html/omit_1/m1_py.html index 44d73e495..94fba21eb 100644 --- a/tests/farm/html/gold_omit_1/m1_py.html +++ b/tests/gold/html/omit_1/m1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m1.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

- -
+

1m1a = 1 

+

2m1b = 2 

- - diff --git a/tests/farm/html/gold_omit_2/m2_py.html b/tests/gold/html/omit_1/m2_py.html similarity index 53% rename from tests/farm/html/gold_omit_2/m2_py.html rename to tests/gold/html/omit_1/m2_py.html index ff75cbe97..ade526d30 100644 --- a/tests/farm/html/gold_omit_2/m2_py.html +++ b/tests/gold/html/omit_1/m2_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m2.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m2a = 1 

-

m2b = 2 

- -
+

1m2a = 1 

+

2m2b = 2 

- - diff --git a/tests/farm/html/gold_omit_2/m3_py.html b/tests/gold/html/omit_1/m3_py.html similarity index 53% rename from tests/farm/html/gold_omit_2/m3_py.html rename to tests/gold/html/omit_1/m3_py.html index 4cdcc0885..d6b4756d1 100644 --- a/tests/farm/html/gold_omit_2/m3_py.html +++ b/tests/gold/html/omit_1/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

- -
+

1m3a = 1 

+

2m3b = 2 

- - diff --git a/tests/gold/html/omit_1/main_py.html b/tests/gold/html/omit_1/main_py.html new file mode 100644 index 000000000..5d4781245 --- /dev/null +++ b/tests/gold/html/omit_1/main_py.html @@ -0,0 +1,74 @@ + + + + + + Coverage for main.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1import m1 

+

2import m2 

+

3import m3 

+

4 

+

5a = 5 

+

6b = 6 

+

7 

+

8assert m1.m1a == 1 

+

9assert m2.m2a == 1 

+

10assert m3.m3a == 1 

+
+ + + diff --git a/tests/farm/html/gold_omit_2/index.html b/tests/gold/html/omit_2/index.html similarity index 90% rename from tests/farm/html/gold_omit_2/index.html rename to tests/gold/html/omit_2/index.html index 01c13e316..5b5e3c6ea 100644 --- a/tests/farm/html/gold_omit_2/index.html +++ b/tests/gold/html/omit_2/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,81 +34,65 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - - - -
Module statements missing excludedcoverage
Total 12 0 0100%
m2.py 2 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - diff --git a/tests/farm/html/gold_omit_1/m2_py.html b/tests/gold/html/omit_2/m2_py.html similarity index 53% rename from tests/farm/html/gold_omit_1/m2_py.html rename to tests/gold/html/omit_2/m2_py.html index ff75cbe97..ade526d30 100644 --- a/tests/farm/html/gold_omit_1/m2_py.html +++ b/tests/gold/html/omit_2/m2_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m2.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m2a = 1 

-

m2b = 2 

- -
+

1m2a = 1 

+

2m2b = 2 

- - diff --git a/tests/farm/html/gold_omit_1/m3_py.html b/tests/gold/html/omit_2/m3_py.html similarity index 53% rename from tests/farm/html/gold_omit_1/m3_py.html rename to tests/gold/html/omit_2/m3_py.html index 4cdcc0885..d6b4756d1 100644 --- a/tests/farm/html/gold_omit_1/m3_py.html +++ b/tests/gold/html/omit_2/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

- -
+

1m3a = 1 

+

2m3b = 2 

- - diff --git a/tests/gold/html/omit_2/main_py.html b/tests/gold/html/omit_2/main_py.html new file mode 100644 index 000000000..5d4781245 --- /dev/null +++ b/tests/gold/html/omit_2/main_py.html @@ -0,0 +1,74 @@ + + + + + + Coverage for main.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1import m1 

+

2import m2 

+

3import m3 

+

4 

+

5a = 5 

+

6b = 6 

+

7 

+

8assert m1.m1a == 1 

+

9assert m2.m2a == 1 

+

10assert m3.m3a == 1 

+
+ + + diff --git a/tests/farm/html/gold_omit_3/index.html b/tests/gold/html/omit_3/index.html similarity index 90% rename from tests/farm/html/gold_omit_3/index.html rename to tests/gold/html/omit_3/index.html index 791929113..f5bc1aaeb 100644 --- a/tests/farm/html/gold_omit_3/index.html +++ b/tests/gold/html/omit_3/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,72 +34,58 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - -
Module statements missing excludedcoverage
Total 10 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - diff --git a/tests/farm/html/gold_omit_3/m3_py.html b/tests/gold/html/omit_3/m3_py.html similarity index 53% rename from tests/farm/html/gold_omit_3/m3_py.html rename to tests/gold/html/omit_3/m3_py.html index 4cdcc0885..d6b4756d1 100644 --- a/tests/farm/html/gold_omit_3/m3_py.html +++ b/tests/gold/html/omit_3/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

- -
+

1m3a = 1 

+

2m3b = 2 

- - diff --git a/tests/gold/html/omit_3/main_py.html b/tests/gold/html/omit_3/main_py.html new file mode 100644 index 000000000..5d4781245 --- /dev/null +++ b/tests/gold/html/omit_3/main_py.html @@ -0,0 +1,74 @@ + + + + + + Coverage for main.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1import m1 

+

2import m2 

+

3import m3 

+

4 

+

5a = 5 

+

6b = 6 

+

7 

+

8assert m1.m1a == 1 

+

9assert m2.m2a == 1 

+

10assert m3.m3a == 1 

+
+ + + diff --git a/tests/farm/html/gold_omit_4/index.html b/tests/gold/html/omit_4/index.html similarity index 90% rename from tests/farm/html/gold_omit_4/index.html rename to tests/gold/html/omit_4/index.html index 0e52f497e..861ba02e3 100644 --- a/tests/farm/html/gold_omit_4/index.html +++ b/tests/gold/html/omit_4/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,81 +34,65 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - - - -
Module statements missing excludedcoverage
Total 12 0 0100%
m1.py 2 0 0100%
m3.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - diff --git a/tests/farm/html/gold_omit_4/m1_py.html b/tests/gold/html/omit_4/m1_py.html similarity index 53% rename from tests/farm/html/gold_omit_4/m1_py.html rename to tests/gold/html/omit_4/m1_py.html index 44d73e495..94fba21eb 100644 --- a/tests/farm/html/gold_omit_4/m1_py.html +++ b/tests/gold/html/omit_4/m1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m1.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

- -
+

1m1a = 1 

+

2m1b = 2 

- - diff --git a/tests/farm/html/gold_omit_4/m3_py.html b/tests/gold/html/omit_4/m3_py.html similarity index 53% rename from tests/farm/html/gold_omit_4/m3_py.html rename to tests/gold/html/omit_4/m3_py.html index 4cdcc0885..d6b4756d1 100644 --- a/tests/farm/html/gold_omit_4/m3_py.html +++ b/tests/gold/html/omit_4/m3_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m3.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m3a = 1 

-

m3b = 2 

- -
+

1m3a = 1 

+

2m3b = 2 

- - diff --git a/tests/gold/html/omit_4/main_py.html b/tests/gold/html/omit_4/main_py.html new file mode 100644 index 000000000..5d4781245 --- /dev/null +++ b/tests/gold/html/omit_4/main_py.html @@ -0,0 +1,74 @@ + + + + + + Coverage for main.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1import m1 

+

2import m2 

+

3import m3 

+

4 

+

5a = 5 

+

6b = 6 

+

7 

+

8assert m1.m1a == 1 

+

9assert m2.m2a == 1 

+

10assert m3.m3a == 1 

+
+ + + diff --git a/tests/farm/html/gold_omit_5/index.html b/tests/gold/html/omit_5/index.html similarity index 90% rename from tests/farm/html/gold_omit_5/index.html rename to tests/gold/html/omit_5/index.html index 509e7900c..8afbebad5 100644 --- a/tests/farm/html/gold_omit_5/index.html +++ b/tests/gold/html/omit_5/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,72 +34,58 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - - - -
Module statements missing excludedcoverage
Total 10 0 0100%
m1.py 2 0 0100%
main.py 8 0 0100%
-

No items found using the specified filter.

- - diff --git a/tests/farm/html/gold_omit_5/m1_py.html b/tests/gold/html/omit_5/m1_py.html similarity index 53% rename from tests/farm/html/gold_omit_5/m1_py.html rename to tests/gold/html/omit_5/m1_py.html index 44d73e495..94fba21eb 100644 --- a/tests/farm/html/gold_omit_5/m1_py.html +++ b/tests/gold/html/omit_5/m1_py.html @@ -1,16 +1,10 @@ - - - - - Coverage for m1.py: 100% - @@ -20,80 +14,53 @@ - -
Hide keyboard shortcuts -

Hot-keys on this page

+

Hot-keys on this page

-

+

r m x p   toggle line displays

-

+

j k   next/prev highlighted chunk

-

+

0   (zero) top of page

-

+

1   (one) first highlighted chunk

-
- - - - - -
-

1

-

2

-

3

-

4

-

5

- -
-

# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 

-

# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt 

-

 

-

m1a = 1 

-

m1b = 2 

- -
+

1m1a = 1 

+

2m1b = 2 

- - diff --git a/tests/gold/html/omit_5/main_py.html b/tests/gold/html/omit_5/main_py.html new file mode 100644 index 000000000..5d4781245 --- /dev/null +++ b/tests/gold/html/omit_5/main_py.html @@ -0,0 +1,74 @@ + + + + + + Coverage for main.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1import m1 

+

2import m2 

+

3import m3 

+

4 

+

5a = 5 

+

6b = 6 

+

7 

+

8assert m1.m1a == 1 

+

9assert m2.m2a == 1 

+

10assert m3.m3a == 1 

+
+ + + diff --git a/tests/gold/html/other/blah_blah_other_py.html b/tests/gold/html/other/blah_blah_other_py.html new file mode 100644 index 000000000..d88e21e56 --- /dev/null +++ b/tests/gold/html/other/blah_blah_other_py.html @@ -0,0 +1,68 @@ + + + + + + Coverage for /private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_08291136/othersrc/other.py: 100% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1# A file in another directory. We're checking that it ends up in the 

+

2# HTML report. 

+

3 

+

4print("This is the other src!") 

+
+ + + diff --git a/tests/gold/html/other/here_py.html b/tests/gold/html/other/here_py.html new file mode 100644 index 000000000..94491fb0a --- /dev/null +++ b/tests/gold/html/other/here_py.html @@ -0,0 +1,70 @@ + + + + + + Coverage for here.py: 75% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1import other 

+

2 

+

3if 1 < 2: 

+

4 h = 3 

+

5else: 

+

6 h = 4 

+
+ + + diff --git a/tests/farm/html/gold_other/index.html b/tests/gold/html/other/index.html similarity index 84% rename from tests/farm/html/gold_other/index.html rename to tests/gold/html/other/index.html index 9a8d72de6..644d14909 100644 --- a/tests/farm/html/gold_other/index.html +++ b/tests/gold/html/other/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,72 +34,58 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - + - - - -
Module statements missing excludedcoverage
Total 5 1 080%
/Users/ned/coverage/trunk/tests/farm/html/othersrc/other.py/private/var/folders/j2/gr3cj3jn63s5q8g3bjvw57hm0000gp/T/coverage_test/tests_test_html_HtmlGoldTests_test_other_08291136/othersrc/other.py 1 0 0100%
here.py 4 1 075%
-

No items found using the specified filter.

- - diff --git a/tests/farm/html/gold_partial/index.html b/tests/gold/html/partial/index.html similarity index 79% rename from tests/farm/html/gold_partial/index.html rename to tests/gold/html/partial/index.html index 40ffcc4c7..f766c0b4c 100644 --- a/tests/farm/html/gold_partial/index.html +++ b/tests/gold/html/partial/index.html @@ -1,15 +1,11 @@ - - - Coverage report - - + @@ -18,21 +14,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -42,75 +34,59 @@

Coverage report: s m x - b p - c   change column sorting

-
- - - - - + - - + - - - + + - - + - - + - - - + + -
Module statements missing excludedbranches partialcoverage
Total87 001 40100%191%
partial.py87 001 40100%191%
-

No items found using the specified filter.

- - diff --git a/tests/gold/html/partial/partial_py.html b/tests/gold/html/partial/partial_py.html new file mode 100644 index 000000000..c301a58a4 --- /dev/null +++ b/tests/gold/html/partial/partial_py.html @@ -0,0 +1,82 @@ + + + + + + Coverage for partial.py: 91% + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1# partial branches and excluded lines 

+

2a = 2 

+

3 

+

4while "no peephole".upper(): # t4 4 ↛ 7line 4 didn't jump to line 7, because the condition on line 4 was never false

+

5 break 

+

6 

+

7while a: # pragma: no branch 

+

8 break 

+

9 

+

10if 0: 

+

11 never_happen() 

+

12 

+

13if 13: 

+

14 a = 14 

+

15 

+

16if a == 16: 

+

17 raise ZeroDivisionError("17") 

+
+ + + diff --git a/tests/gold/html/styled/a_py.html b/tests/gold/html/styled/a_py.html new file mode 100644 index 000000000..b81641cca --- /dev/null +++ b/tests/gold/html/styled/a_py.html @@ -0,0 +1,70 @@ + + + + + + Coverage for a.py: 67% + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1if 1 < 2: 

+

2 # Needed a < to look at HTML entities. 

+

3 a = 3 

+

4else: 

+

5 a = 4 

+
+ + + diff --git a/tests/farm/html/gold_styled/extra.css b/tests/gold/html/styled/extra.css similarity index 100% rename from tests/farm/html/gold_styled/extra.css rename to tests/gold/html/styled/extra.css diff --git a/tests/farm/html/gold_styled/index.html b/tests/gold/html/styled/index.html similarity index 90% rename from tests/farm/html/gold_styled/index.html rename to tests/gold/html/styled/index.html index 05a8fe21b..d1a9259b6 100644 --- a/tests/farm/html/gold_styled/index.html +++ b/tests/gold/html/styled/index.html @@ -1,17 +1,12 @@ - - - Coverage report - - - + @@ -20,21 +15,17 @@ - -
Hide keyboard shortcuts

Hot-keys on this page

@@ -44,63 +35,51 @@

Coverage report: s m x - c   change column sorting

-
- - - - - - -
Module statements missing excludedcoverage
Total 3 1 067%
a.py 3 1 067%
-

No items found using the specified filter.

- - diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css new file mode 100644 index 000000000..3e7f9b66b --- /dev/null +++ b/tests/gold/html/styled/style.css @@ -0,0 +1,291 @@ +@charset "UTF-8"; +/* Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 */ +/* For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt */ +/* Don't edit this .css file. Edit the .scss file instead! */ +html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } + +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } + +html > body { font-size: 16px; } + +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } + +table { border-collapse: collapse; } + +td { vertical-align: top; } + +table tr.hidden { display: none !important; } + +p#no_rows { display: none; font-size: 1.2em; } + +a.nav { text-decoration: none; color: inherit; } + +a.nav:hover { text-decoration: underline; color: inherit; } + +#header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #header { background: black; } } + +@media (prefers-color-scheme: dark) { #header { border-color: #333; } } + +.indexfile #footer { margin: 1rem 3rem; } + +.pyfile #footer { margin: 1rem 1rem; } + +#footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3rem; } + +#header .content { padding: 1rem 3rem; } + +h1 { font-size: 1.25em; display: inline-block; } + +#filter_container { float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } + +#filter_container input:focus { border-color: #007acc; } + +h2.stats { margin-top: .5em; font-size: 1em; } + +.stats button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { .stats button { border-color: #444; } } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { .stats button.run { background: #373d29; } } + +.stats button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.run.show_run { background: #373d29; } } + +.stats button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { .stats button.mis { background: #4b1818; } } + +.stats button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.mis.show_mis { background: #4b1818; } } + +.stats button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { .stats button.exc { background: #333; } } + +.stats button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.exc.show_exc { background: #333; } } + +.stats button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { .stats button.par { background: #650; } } + +.stats button.par.show_par { background: #ffa; border: 2px solid #dddd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.par.show_par { background: #650; } } + +.help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } + +#source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } + +#keyboard_icon { float: right; margin: 5px; cursor: pointer; } + +.help_panel { padding: .5em; border: 1px solid #883; } + +.help_panel .legend { font-style: italic; margin-bottom: 1em; } + +.indexfile .help_panel { width: 20em; min-height: 4em; } + +.pyfile .help_panel { width: 16em; min-height: 8em; } + +#panel_icon { float: right; cursor: pointer; } + +.keyhelp { margin: .75em; } + +.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } + +#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + +#source p { position: relative; white-space: pre; } + +#source p * { box-sizing: border-box; } + +#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n a { text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + +#source p.highlight .n { background: #ffdd00; } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + +#source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + +#source p .t:hover ~ .r .annotate.long { display: block; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6A9955; } } + +#source p .t .key { font-weight: bold; line-height: 1px; } + +#source p .t .str { color: #0451A5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { color: #9CDCFE; } } + +#source p.mis .t { border-left: 0.2em solid #ff0000; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + +#source p.mis.show_mis .t:hover { background: #f2d2d2; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + +#source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + +#source p.exc .t { border-left: 0.2em solid #808080; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + +#source p.exc.show_exc .t:hover { background: #e2e2e2; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #dddd00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + +#source p.par.show_par .t:hover { background: #f2f2a2; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + +#source p .annotate.short:hover ~ .long { display: block; } + +#source p .annotate.long { width: 30em; right: 2.5em; } + +#source p input { display: none; } + +#source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + +#source p input ~ .r label.ctx::before { content: "▶ "; } + +#source p input ~ .r label.ctx:hover { background: #d5f7ff; color: #666; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #aef; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + +#source p input:checked ~ .r label.ctx::before { content: "▼ "; } + +#source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + +#source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #aef; border-radius: .25em; margin-right: 1.75em; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + +#source p .ctxs span { display: block; text-align: right; } + +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + +#index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + +#index td.name, #index th.name { text-align: left; width: auto; } + +#index th { font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th.headerSortDown, #index th.headerSortUp { white-space: nowrap; background: #eee; } + +@media (prefers-color-scheme: dark) { #index th.headerSortDown, #index th.headerSortUp { background: #333; } } + +#index th.headerSortDown:after { content: " ↑"; } + +#index th.headerSortUp:after { content: " ↓"; } + +#index td.name a { text-decoration: none; color: inherit; } + +#index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } + +#index tr.file:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } + +#index tr.file:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/tests/gold/html/support/coverage_html.js b/tests/gold/html/support/coverage_html.js new file mode 100644 index 000000000..22152333e --- /dev/null +++ b/tests/gold/html/support/coverage_html.js @@ -0,0 +1,584 @@ +// Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +// For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +// Coverage.py HTML report browser code. +/*jslint browser: true, sloppy: true, vars: true, plusplus: true, maxerr: 50, indent: 4 */ +/*global coverage: true, document, window, $ */ + +coverage = {}; + +// Find all the elements with shortkey_* class, and use them to assign a shortcut key. +coverage.assign_shortkeys = function () { + $("*[class*='shortkey_']").each(function (i, e) { + $.each($(e).attr("class").split(" "), function (i, c) { + if (/^shortkey_/.test(c)) { + $(document).bind('keydown', c.substr(9), function () { + $(e).click(); + }); + } + }); + }); +}; + +// Create the events for the help panel. +coverage.wire_up_help_panel = function () { + $("#keyboard_icon").click(function () { + // Show the help panel, and position it so the keyboard icon in the + // panel is in the same place as the keyboard icon in the header. + $(".help_panel").show(); + var koff = $("#keyboard_icon").offset(); + var poff = $("#panel_icon").position(); + $(".help_panel").offset({ + top: koff.top-poff.top, + left: koff.left-poff.left + }); + }); + $("#panel_icon").click(function () { + $(".help_panel").hide(); + }); +}; + +// Create the events for the filter box. +coverage.wire_up_filter = function () { + // Cache elements. + var table = $("table.index"); + var table_rows = table.find("tbody tr"); + var table_row_names = table_rows.find("td.name a"); + var no_rows = $("#no_rows"); + + // Create a duplicate table footer that we can modify with dynamic summed values. + var table_footer = $("table.index tfoot tr"); + var table_dynamic_footer = table_footer.clone(); + table_dynamic_footer.attr('class', 'total_dynamic hidden'); + table_footer.after(table_dynamic_footer); + + // Observe filter keyevents. + $("#filter").on("keyup change", $.debounce(150, function (event) { + var filter_value = $(this).val(); + + if (filter_value === "") { + // Filter box is empty, remove all filtering. + table_rows.removeClass("hidden"); + + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + + // Hide placeholder, show table. + if (no_rows.length > 0) { + no_rows.hide(); + } + table.show(); + + } + else { + // Filter table items by value. + var hidden = 0; + var shown = 0; + + // Hide / show elements. + $.each(table_row_names, function () { + var element = $(this).parents("tr"); + + if ($(this).text().indexOf(filter_value) === -1) { + // hide + element.addClass("hidden"); + hidden++; + } + else { + // show + element.removeClass("hidden"); + shown++; + } + }); + + // Show placeholder if no rows will be displayed. + if (no_rows.length > 0) { + if (shown === 0) { + // Show placeholder, hide table. + no_rows.show(); + table.hide(); + } + else { + // Hide placeholder, show table. + no_rows.hide(); + table.show(); + } + } + + // Manage dynamic header: + if (hidden > 0) { + // Calculate new dynamic sum values based on visible rows. + for (var column = 2; column < 20; column++) { + // Calculate summed value. + var cells = table_rows.find('td:nth-child(' + column + ')'); + if (!cells.length) { + // No more columns...! + break; + } + + var sum = 0, numer = 0, denom = 0; + $.each(cells.filter(':visible'), function () { + var ratio = $(this).data("ratio"); + if (ratio) { + var splitted = ratio.split(" "); + numer += parseInt(splitted[0], 10); + denom += parseInt(splitted[1], 10); + } + else { + sum += parseInt(this.innerHTML, 10); + } + }); + + // Get footer cell element. + var footer_cell = table_dynamic_footer.find('td:nth-child(' + column + ')'); + + // Set value into dynamic footer cell element. + if (cells[0].innerHTML.indexOf('%') > -1) { + // Percentage columns use the numerator and denominator, + // and adapt to the number of decimal places. + var match = /\.([0-9]+)/.exec(cells[0].innerHTML); + var places = 0; + if (match) { + places = match[1].length; + } + var pct = numer * 100 / denom; + footer_cell.text(pct.toFixed(places) + '%'); + } + else { + footer_cell.text(sum); + } + } + + // Hide standard footer, show dynamic footer. + table_footer.addClass("hidden"); + table_dynamic_footer.removeClass("hidden"); + } + else { + // Show standard footer, hide dynamic footer. + table_footer.removeClass("hidden"); + table_dynamic_footer.addClass("hidden"); + } + } + })); + + // Trigger change event on setup, to force filter on page refresh + // (filter value may still be present). + $("#filter").trigger("change"); +}; + +// Loaded on index.html +coverage.index_ready = function ($) { + // Look for a localStorage item containing previous sort settings: + var sort_list = []; + var storage_name = "COVERAGE_INDEX_SORT"; + var stored_list = localStorage.getItem(storage_name); + + if (stored_list) { + sort_list = JSON.parse('[[' + stored_list + ']]'); + } + + // Create a new widget which exists only to save and restore + // the sort order: + $.tablesorter.addWidget({ + id: "persistentSort", + + // Format is called by the widget before displaying: + format: function (table) { + if (table.config.sortList.length === 0 && sort_list.length > 0) { + // This table hasn't been sorted before - we'll use + // our stored settings: + $(table).trigger('sorton', [sort_list]); + } + else { + // This is not the first load - something has + // already defined sorting so we'll just update + // our stored value to match: + sort_list = table.config.sortList; + } + } + }); + + // Configure our tablesorter to handle the variable number of + // columns produced depending on report options: + var headers = []; + var col_count = $("table.index > thead > tr > th").length; + + headers[0] = { sorter: 'text' }; + for (i = 1; i < col_count-1; i++) { + headers[i] = { sorter: 'digit' }; + } + headers[col_count-1] = { sorter: 'percent' }; + + // Enable the table sorter: + $("table.index").tablesorter({ + widgets: ['persistentSort'], + headers: headers + }); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + coverage.wire_up_filter(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + localStorage.setItem(storage_name, sort_list.toString()) + }); +}; + +// -- pyfile stuff -- + +coverage.pyfile_ready = function ($) { + // If we're directed to a particular line number, highlight the line. + var frag = location.hash; + if (frag.length > 2 && frag[1] === 't') { + $(frag).addClass('highlight'); + coverage.set_sel(parseInt(frag.substr(2), 10)); + } + else { + coverage.set_sel(0); + } + + $(document) + .bind('keydown', 'j', coverage.to_next_chunk_nicely) + .bind('keydown', 'k', coverage.to_prev_chunk_nicely) + .bind('keydown', '0', coverage.to_top) + .bind('keydown', '1', coverage.to_first_chunk) + ; + + $(".button_toggle_run").click(function (evt) {coverage.toggle_lines(evt.target, "run");}); + $(".button_toggle_exc").click(function (evt) {coverage.toggle_lines(evt.target, "exc");}); + $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); + $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + + coverage.assign_shortkeys(); + coverage.wire_up_help_panel(); + + coverage.init_scroll_markers(); + + // Rebuild scroll markers when the window height changes. + $(window).resize(coverage.build_scroll_markers); +}; + +coverage.toggle_lines = function (btn, cls) { + btn = $(btn); + var show = "show_"+cls; + if (btn.hasClass(show)) { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); + } + else { + $("#source ." + cls).addClass(show); + btn.addClass(show); + } + coverage.build_scroll_markers(); +}; + +// Return the nth line div. +coverage.line_elt = function (n) { + return $("#t" + n); +}; + +// Return the nth line number div. +coverage.num_elt = function (n) { + return $("#n" + n); +}; + +// Set the selection. b and e are line numbers. +coverage.set_sel = function (b, e) { + // The first line selected. + coverage.sel_begin = b; + // The next line not selected. + coverage.sel_end = (e === undefined) ? b+1 : e; +}; + +coverage.to_top = function () { + coverage.set_sel(0, 1); + coverage.scroll_window(0); +}; + +coverage.to_first_chunk = function () { + coverage.set_sel(0, 1); + coverage.to_next_chunk(); +}; + +// Return a string indicating what kind of chunk this line belongs to, +// or null if not a chunk. +coverage.chunk_indicator = function (line_elt) { + var klass = line_elt.attr('class'); + if (klass) { + var m = klass.match(/\bshow_\w+\b/); + if (m) { + return m[0]; + } + } + return null; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + var chunk_indicator, probe_line; + while (true) { + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + if (chunk_indicator) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_indicator = chunk_indicator; + while (next_indicator === chunk_indicator) { + probe++; + probe_line = c.line_elt(probe); + next_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(begin, probe); + c.show_selection(); +}; + +coverage.to_prev_chunk = function () { + var c = coverage; + + // Find the end of the prev colored chunk. + var probe = c.sel_begin-1; + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var chunk_indicator = c.chunk_indicator(probe_line); + while (probe > 0 && !chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + chunk_indicator = c.chunk_indicator(probe_line); + } + + // There's a prev chunk, `probe` points to its last line. + var end = probe+1; + + // Find the beginning of this chunk. + var prev_indicator = chunk_indicator; + while (prev_indicator === chunk_indicator) { + probe--; + probe_line = c.line_elt(probe); + prev_indicator = c.chunk_indicator(probe_line); + } + c.set_sel(probe+1, end); + c.show_selection(); +}; + +// Return the line number of the line nearest pixel position pos +coverage.line_at_pos = function (pos) { + var l1 = coverage.line_elt(1), + l2 = coverage.line_elt(2), + result; + if (l1.length && l2.length) { + var l1_top = l1.offset().top, + line_height = l2.offset().top - l1_top, + nlines = (pos - l1_top) / line_height; + if (nlines < 1) { + result = 1; + } + else { + result = Math.ceil(nlines); + } + } + else { + result = 1; + } + return result; +}; + +// Returns 0, 1, or 2: how many of the two ends of the selection are on +// the screen right now? +coverage.selection_ends_on_screen = function () { + if (coverage.sel_begin === 0) { + return 0; + } + + var top = coverage.line_elt(coverage.sel_begin); + var next = coverage.line_elt(coverage.sel_end-1); + + return ( + (top.isOnScreen() ? 1 : 0) + + (next.isOnScreen() ? 1 : 0) + ); +}; + +coverage.to_next_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + // The selection is entirely off the screen: select the top line on + // the screen. + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop())); + } + coverage.to_next_chunk(); +}; + +coverage.to_prev_chunk_nicely = function () { + coverage.finish_scrolling(); + if (coverage.selection_ends_on_screen() === 0) { + var win = $(window); + coverage.select_line_or_chunk(coverage.line_at_pos(win.scrollTop() + win.height())); + } + coverage.to_prev_chunk(); +}; + +// Select line number lineno, or if it is in a colored chunk, select the +// entire chunk +coverage.select_line_or_chunk = function (lineno) { + var c = coverage; + var probe_line = c.line_elt(lineno); + if (probe_line.length === 0) { + return; + } + var the_indicator = c.chunk_indicator(probe_line); + if (the_indicator) { + // The line is in a highlighted chunk. + // Search backward for the first line. + var probe = lineno; + var indicator = the_indicator; + while (probe > 0 && indicator === the_indicator) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + indicator = c.chunk_indicator(probe_line); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + indicator = the_indicator; + while (indicator === the_indicator) { + probe++; + probe_line = c.line_elt(probe); + indicator = c.chunk_indicator(probe_line); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + $(".linenos .highlight").removeClass("highlight"); + for (var probe = c.sel_begin; probe > 0 && probe < c.sel_end; probe++) { + c.num_elt(probe).addClass("highlight"); + } + + c.scroll_to_selection(); +}; + +coverage.scroll_to_selection = function () { + // Scroll the page if the chunk isn't fully visible. + if (coverage.selection_ends_on_screen() < 2) { + // Need to move the page. The html,body trick makes it scroll in all + // browsers, got it from http://stackoverflow.com/questions/3042651 + var top = coverage.line_elt(coverage.sel_begin); + var top_pos = parseInt(top.offset().top, 10); + coverage.scroll_window(top_pos - 30); + } +}; + +coverage.scroll_window = function (to_pos) { + $("html,body").animate({scrollTop: to_pos}, 200); +}; + +coverage.finish_scrolling = function () { + $("html,body").stop(true, true); +}; + +coverage.init_scroll_markers = function () { + var c = coverage; + // Init some variables + c.lines_len = $('#source p').length; + c.body_h = $('body').height(); + c.header_h = $('div#header').height(); + + // Build html + c.build_scroll_markers(); +}; + +coverage.build_scroll_markers = function () { + var c = coverage, + min_line_height = 3, + max_line_height = 10, + visible_window_h = $(window).height(); + + c.lines_to_mark = $('#source').find('p.show_run, p.show_mis, p.show_exc, p.show_exc, p.show_par'); + $('#scroll_marker').remove(); + // Don't build markers if the window has no scroll bar. + if (c.body_h <= visible_window_h) { + return; + } + + $("body").append("
 
"); + var scroll_marker = $('#scroll_marker'), + marker_scale = scroll_marker.height() / c.body_h, + line_height = scroll_marker.height() / c.lines_len; + + // Line height must be between the extremes. + if (line_height > min_line_height) { + if (line_height > max_line_height) { + line_height = max_line_height; + } + } + else { + line_height = min_line_height; + } + + var previous_line = -99, + last_mark, + last_top, + offsets = {}; + + // Calculate line offsets outside loop to prevent relayouts + c.lines_to_mark.each(function() { + offsets[this.id] = $(this).offset().top; + }); + c.lines_to_mark.each(function () { + var id_name = $(this).attr('id'), + line_top = Math.round(offsets[id_name] * marker_scale), + line_number = parseInt(id_name.substring(1, id_name.length)); + + if (line_number === previous_line + 1) { + // If this solid missed block just make previous mark higher. + last_mark.css({ + 'height': line_top + line_height - last_top + }); + } + else { + // Add colored line in scroll_marker block. + scroll_marker.append('
'); + last_mark = $('#m' + line_number); + last_mark.css({ + 'height': line_height, + 'top': line_top + }); + last_top = line_top; + } + + previous_line = line_number; + }); +}; diff --git a/tests/gold/html/support/jquery.ba-throttle-debounce.min.js b/tests/gold/html/support/jquery.ba-throttle-debounce.min.js new file mode 100644 index 000000000..648fe5d3c --- /dev/null +++ b/tests/gold/html/support/jquery.ba-throttle-debounce.min.js @@ -0,0 +1,9 @@ +/* + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * Copyright (c) 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + */ +(function(b,c){var $=b.jQuery||b.Cowboy||(b.Cowboy={}),a;$.throttle=a=function(e,f,j,i){var h,d=0;if(typeof f!=="boolean"){i=j;j=f;f=c}function g(){var o=this,m=+new Date()-d,n=arguments;function l(){d=+new Date();j.apply(o,n)}function k(){h=c}if(i&&!h){l()}h&&clearTimeout(h);if(i===c&&m>e){l()}else{if(f!==true){h=setTimeout(i?k:l,i===c?e-m:e)}}}if($.guid){g.guid=j.guid=j.guid||$.guid++}return g};$.debounce=function(d,e,f){return f===c?a(d,e,false):a(d,f,e!==false)}})(this); diff --git a/tests/gold/html/support/jquery.hotkeys.js b/tests/gold/html/support/jquery.hotkeys.js new file mode 100644 index 000000000..09b21e03c --- /dev/null +++ b/tests/gold/html/support/jquery.hotkeys.js @@ -0,0 +1,99 @@ +/* + * jQuery Hotkeys Plugin + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * + * Based upon the plugin by Tzury Bar Yochay: + * http://github.com/tzuryby/hotkeys + * + * Original idea by: + * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ +*/ + +(function(jQuery){ + + jQuery.hotkeys = { + version: "0.8", + + specialKeys: { + 8: "backspace", 9: "tab", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", + 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", + 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", + 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", + 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", + 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", + 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 191: "/", 224: "meta" + }, + + shiftNums: { + "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", + "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", + ".": ">", "/": "?", "\\": "|" + } + }; + + function keyHandler( handleObj ) { + // Only care when a possible input has been specified + if ( typeof handleObj.data !== "string" ) { + return; + } + + var origHandler = handleObj.handler, + keys = handleObj.data.toLowerCase().split(" "); + + handleObj.handler = function( event ) { + // Don't fire in text-accepting inputs that we didn't directly bind to + if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || + event.target.type === "text") ) { + return; + } + + // Keypress represents characters, not special keys + var special = event.type !== "keypress" && jQuery.hotkeys.specialKeys[ event.which ], + character = String.fromCharCode( event.which ).toLowerCase(), + key, modif = "", possible = {}; + + // check combinations (alt|ctrl|shift+anything) + if ( event.altKey && special !== "alt" ) { + modif += "alt+"; + } + + if ( event.ctrlKey && special !== "ctrl" ) { + modif += "ctrl+"; + } + + // TODO: Need to make sure this works consistently across platforms + if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { + modif += "meta+"; + } + + if ( event.shiftKey && special !== "shift" ) { + modif += "shift+"; + } + + if ( special ) { + possible[ modif + special ] = true; + + } else { + possible[ modif + character ] = true; + possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; + + // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" + if ( modif === "shift+" ) { + possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; + } + } + + for ( var i = 0, l = keys.length; i < l; i++ ) { + if ( possible[ keys[i] ] ) { + return origHandler.apply( this, arguments ); + } + } + }; + } + + jQuery.each([ "keydown", "keyup", "keypress" ], function() { + jQuery.event.special[ this ] = { add: keyHandler }; + }); + +})( jQuery ); diff --git a/tests/gold/html/support/jquery.isonscreen.js b/tests/gold/html/support/jquery.isonscreen.js new file mode 100644 index 000000000..0182ebd21 --- /dev/null +++ b/tests/gold/html/support/jquery.isonscreen.js @@ -0,0 +1,53 @@ +/* Copyright (c) 2010 + * @author Laurence Wheway + * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) + * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. + * + * @version 1.2.0 + */ +(function($) { + jQuery.extend({ + isOnScreen: function(box, container) { + //ensure numbers come in as intgers (not strings) and remove 'px' is it's there + for(var i in box){box[i] = parseFloat(box[i])}; + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( box.left+box.width-container.left > 0 && + box.left < container.width+container.left && + box.top+box.height-container.top > 0 && + box.top < container.height+container.top + ) return true; + return false; + } + }) + + + jQuery.fn.isOnScreen = function (container) { + for(var i in container){container[i] = parseFloat(container[i])}; + + if(!container){ + container = { + left: $(window).scrollLeft(), + top: $(window).scrollTop(), + width: $(window).width(), + height: $(window).height() + } + } + + if( $(this).offset().left+$(this).width()-container.left > 0 && + $(this).offset().left < container.width+container.left && + $(this).offset().top+$(this).height()-container.top > 0 && + $(this).offset().top < container.height+container.top + ) return true; + return false; + } +})(jQuery); diff --git a/tests/gold/html/support/jquery.min.js b/tests/gold/html/support/jquery.min.js new file mode 100644 index 000000000..d1608e37f --- /dev/null +++ b/tests/gold/html/support/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.11.1 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.1",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+-new Date,v=a.document,w=0,x=0,y=gb(),z=gb(),A=gb(),B=function(a,b){return a===b&&(l=!0),0},C="undefined",D=1<<31,E={}.hasOwnProperty,F=[],G=F.pop,H=F.push,I=F.push,J=F.slice,K=F.indexOf||function(a){for(var b=0,c=this.length;c>b;b++)if(this[b]===a)return b;return-1},L="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",N="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",O=N.replace("w","w#"),P="\\["+M+"*("+N+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+O+"))|)"+M+"*\\]",Q=":("+N+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+P+")*)|.*)\\)|)",R=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),S=new RegExp("^"+M+"*,"+M+"*"),T=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),U=new RegExp("="+M+"*([^\\]'\"]*?)"+M+"*\\]","g"),V=new RegExp(Q),W=new RegExp("^"+O+"$"),X={ID:new RegExp("^#("+N+")"),CLASS:new RegExp("^\\.("+N+")"),TAG:new RegExp("^("+N.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+Q),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+M+"?|("+M+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)};try{I.apply(F=J.call(v.childNodes),v.childNodes),F[v.childNodes.length].nodeType}catch(eb){I={apply:F.length?function(a,b){H.apply(a,J.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],!a||"string"!=typeof a)return d;if(1!==(k=b.nodeType)&&9!==k)return[];if(p&&!e){if(f=_.exec(a))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return I.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName&&b.getElementsByClassName)return I.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=9===k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+qb(o[l]);w=ab.test(a)&&ob(b.parentNode)||b,x=o.join(",")}if(x)try{return I.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function gb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function hb(a){return a[u]=!0,a}function ib(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function jb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function kb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||D)-(~a.sourceIndex||D);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function lb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function nb(a){return hb(function(b){return b=+b,hb(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function ob(a){return a&&typeof a.getElementsByTagName!==C&&a}c=fb.support={},f=fb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fb.setDocument=function(a){var b,e=a?a.ownerDocument||a:v,g=e.defaultView;return e!==n&&9===e.nodeType&&e.documentElement?(n=e,o=e.documentElement,p=!f(e),g&&g!==g.top&&(g.addEventListener?g.addEventListener("unload",function(){m()},!1):g.attachEvent&&g.attachEvent("onunload",function(){m()})),c.attributes=ib(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ib(function(a){return a.appendChild(e.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(e.getElementsByClassName)&&ib(function(a){return a.innerHTML="
",a.firstChild.className="i",2===a.getElementsByClassName("i").length}),c.getById=ib(function(a){return o.appendChild(a).id=u,!e.getElementsByName||!e.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if(typeof b.getElementById!==C&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c=typeof a.getAttributeNode!==C&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return typeof b.getElementsByTagName!==C?b.getElementsByTagName(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return typeof b.getElementsByClassName!==C&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(e.querySelectorAll))&&(ib(function(a){a.innerHTML="",a.querySelectorAll("[msallowclip^='']").length&&q.push("[*^$]="+M+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+M+"*(?:value|"+L+")"),a.querySelectorAll(":checked").length||q.push(":checked")}),ib(function(a){var b=e.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+M+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ib(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",Q)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===e||a.ownerDocument===v&&t(v,a)?-1:b===e||b.ownerDocument===v&&t(v,b)?1:k?K.call(k,a)-K.call(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,f=a.parentNode,g=b.parentNode,h=[a],i=[b];if(!f||!g)return a===e?-1:b===e?1:f?-1:g?1:k?K.call(k,a)-K.call(k,b):0;if(f===g)return kb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?kb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},e):n},fb.matches=function(a,b){return fb(a,null,null,b)},fb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fb(b,n,null,[a]).length>0},fb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&E.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fb.selectors={cacheLength:50,createPseudo:hb,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+M+")"+a+"("+M+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||typeof a.getAttribute!==C&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?hb(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=K.call(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:hb(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?hb(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),!c.pop()}}),has:hb(function(a){return function(b){return fb(a,b).length>0}}),contains:hb(function(a){return function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:hb(function(a){return W.test(a||"")||fb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:nb(function(){return[0]}),last:nb(function(a,b){return[b-1]}),eq:nb(function(a,b,c){return[0>c?c+b:c]}),even:nb(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:nb(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:nb(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:nb(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function rb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function sb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function tb(a,b,c){for(var d=0,e=b.length;e>d;d++)fb(a,b[d],c);return c}function ub(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function vb(a,b,c,d,e,f){return d&&!d[u]&&(d=vb(d)),e&&!e[u]&&(e=vb(e,f)),hb(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||tb(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ub(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ub(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?K.call(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ub(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):I.apply(g,r)})}function wb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=rb(function(a){return a===b},h,!0),l=rb(function(a){return K.call(b,a)>-1},h,!0),m=[function(a,c,d){return!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d))}];f>i;i++)if(c=d.relative[a[i].type])m=[rb(sb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return vb(i>1&&sb(m),i>1&&qb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&wb(a.slice(i,e)),f>e&&wb(a=a.slice(e)),f>e&&qb(a))}m.push(c)}return sb(m)}function xb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=G.call(i));s=ub(s)}I.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&fb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?hb(f):f}return h=fb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xb(e,d)),f.selector=a}return f},i=fb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&ob(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qb(j),!a)return I.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&ob(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ib(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ib(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||jb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ib(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||jb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ib(function(a){return null==a.getAttribute("disabled")})||jb(L,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h; +if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="
a",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/\s*$/g,rb={option:[1,""],legend:[1,"
","
"],area:[1,"",""],param:[1,"",""],thead:[1,"","
"],tr:[2,"","
"],col:[2,"","
"],td:[3,"","
"],_default:k.htmlSerialize?[0,"",""]:[1,"X
","
"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?""!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("