From 81a8e17dc84157f7b4d9e312b8d72cf1ea88eb37 Mon Sep 17 00:00:00 2001 From: Patrick McCarty Date: Wed, 6 Mar 2013 11:08:23 -0800 Subject: [PATCH] Imported Upstream version 3.6 --- AUTHORS.txt | 41 + CHANGES.txt | 943 +++++++++++ MANIFEST.in | 16 + PKG-INFO | 38 + README.txt | 9 + coverage.egg-info/PKG-INFO | 38 + coverage.egg-info/SOURCES.txt | 237 +++ coverage.egg-info/dependency_links.txt | 1 + coverage.egg-info/entry_points.txt | 5 + coverage.egg-info/not-zip-safe | 1 + coverage.egg-info/top_level.txt | 1 + coverage/__init__.py | 120 ++ coverage/__main__.py | 4 + coverage/annotate.py | 101 ++ coverage/backward.py | 147 ++ coverage/bytecode.py | 91 + coverage/cmdline.py | 730 +++++++++ coverage/codeunit.py | 145 ++ coverage/collector.py | 340 ++++ coverage/config.py | 211 +++ coverage/control.py | 720 ++++++++ coverage/data.py | 270 +++ coverage/execfile.py | 138 ++ coverage/files.py | 289 ++++ coverage/fullcoverage/encodings.py | 57 + coverage/html.py | 367 +++++ coverage/htmlfiles/coverage_html.js | 376 +++++ coverage/htmlfiles/index.html | 104 ++ coverage/htmlfiles/jquery-1.4.3.min.js | 166 ++ coverage/htmlfiles/jquery.hotkeys.js | 99 ++ coverage/htmlfiles/jquery.isonscreen.js | 53 + coverage/htmlfiles/jquery.tablesorter.min.js | 2 + coverage/htmlfiles/keybd_closed.png | Bin 0 -> 264 bytes coverage/htmlfiles/keybd_open.png | Bin 0 -> 267 bytes coverage/htmlfiles/pyfile.html | 90 + coverage/htmlfiles/style.css | 300 ++++ coverage/misc.py | 159 ++ coverage/parser.py | 662 ++++++++ coverage/phystokens.py | 206 +++ coverage/report.py | 92 ++ coverage/results.py | 254 +++ coverage/summary.py | 86 + coverage/templite.py | 166 ++ coverage/tracer.c | 730 +++++++++ coverage/version.py | 9 + coverage/xmlreport.py | 150 ++ doc/api.rst | 47 + doc/branch.rst | 120 ++ doc/changes.rst | 629 +++++++ doc/cmd.rst | 365 +++++ doc/config.rst | 198 +++ doc/contributing.rst | 164 ++ doc/excluding.rst | 105 ++ doc/faq.rst | 116 ++ doc/index.rst | 171 ++ doc/install.rst | 80 + doc/source.rst | 74 + doc/subprocess.rst | 66 + doc/trouble.rst | 88 + igor.py | 255 +++ requirements.txt | 5 + setup.cfg | 5 + setup.py | 199 +++ test/__init__.py | 1 + test/backtest.py | 49 + test/backunittest.py | 115 ++ test/coveragetest.py | 465 ++++++ test/covmodzip1.py | 3 + test/eggsrc/egg1/__init__.py | 0 test/eggsrc/egg1/egg1.py | 4 + test/eggsrc/setup.py | 8 + test/farm/annotate/annotate_dir.py | 7 + test/farm/annotate/gold/white.py,cover | 33 + .../annotate/gold_anno_dir/a___init__.py,cover | 0 test/farm/annotate/gold_anno_dir/a_a.py,cover | 5 + .../annotate/gold_anno_dir/b___init__.py,cover | 0 test/farm/annotate/gold_anno_dir/b_b.py,cover | 3 + test/farm/annotate/gold_anno_dir/multi.py,cover | 5 + test/farm/annotate/gold_multi/a/__init__.py,cover | 0 test/farm/annotate/gold_multi/a/a.py,cover | 5 + test/farm/annotate/gold_multi/b/__init__.py,cover | 0 test/farm/annotate/gold_multi/b/b.py,cover | 2 + test/farm/annotate/gold_multi/multi.py,cover | 5 + test/farm/annotate/gold_v24/white.py,cover | 33 + test/farm/annotate/run.py | 7 + test/farm/annotate/run_multi.py | 7 + test/farm/annotate/src/a/__init__.py | 0 test/farm/annotate/src/a/a.py | 5 + test/farm/annotate/src/b/__init__.py | 0 test/farm/annotate/src/b/b.py | 3 + test/farm/annotate/src/multi.py | 5 + test/farm/annotate/src/white.py | 33 + test/farm/html/gold_a/a.html | 95 ++ test/farm/html/gold_a/index.html | 89 + test/farm/html/gold_b_branch/b.html | 139 ++ test/farm/html/gold_b_branch/index.html | 101 ++ test/farm/html/gold_bom/bom.html | 104 ++ test/farm/html/gold_bom/index.html | 90 + test/farm/html/gold_isolatin1/index.html | 89 + test/farm/html/gold_isolatin1/isolatin1.html | 91 + test/farm/html/gold_omit_1/index.html | 116 ++ test/farm/html/gold_omit_1/m1.html | 85 + test/farm/html/gold_omit_1/m2.html | 85 + test/farm/html/gold_omit_1/m3.html | 85 + test/farm/html/gold_omit_1/main.html | 101 ++ test/farm/html/gold_omit_2/index.html | 107 ++ test/farm/html/gold_omit_2/m2.html | 85 + test/farm/html/gold_omit_2/m3.html | 85 + test/farm/html/gold_omit_2/main.html | 101 ++ test/farm/html/gold_omit_3/index.html | 98 ++ test/farm/html/gold_omit_3/m3.html | 85 + test/farm/html/gold_omit_3/main.html | 101 ++ test/farm/html/gold_omit_4/index.html | 107 ++ test/farm/html/gold_omit_4/m1.html | 85 + test/farm/html/gold_omit_4/m3.html | 85 + test/farm/html/gold_omit_4/main.html | 101 ++ test/farm/html/gold_omit_5/index.html | 98 ++ test/farm/html/gold_omit_5/m1.html | 85 + test/farm/html/gold_omit_5/main.html | 101 ++ test/farm/html/gold_other/blah_blah_other.html | 89 + test/farm/html/gold_other/here.html | 97 ++ test/farm/html/gold_other/index.html | 98 ++ test/farm/html/gold_partial/index.html | 101 ++ test/farm/html/gold_partial/partial.html | 121 ++ test/farm/html/gold_styled/a.html | 95 ++ test/farm/html/gold_styled/extra.css | 1 + test/farm/html/gold_styled/index.html | 89 + test/farm/html/gold_styled/style.css | 275 ++++ test/farm/html/gold_unicode/index.html | 89 + test/farm/html/gold_unicode/unicode.html | 91 + test/farm/html/gold_x_xml/coverage.xml | 20 + test/farm/html/gold_y_xml_branch/coverage.xml | 22 + test/farm/html/othersrc/other.py | 4 + test/farm/html/run_a.py | 25 + test/farm/html/run_a_xml_1.py | 21 + test/farm/html/run_a_xml_2.py | 21 + test/farm/html/run_b_branch.py | 28 + test/farm/html/run_bom.py | 21 + test/farm/html/run_isolatin1.py | 21 + test/farm/html/run_omit_1.py | 12 + test/farm/html/run_omit_2.py | 12 + test/farm/html/run_omit_3.py | 12 + test/farm/html/run_omit_4.py | 12 + test/farm/html/run_omit_5.py | 12 + test/farm/html/run_other.py | 26 + test/farm/html/run_partial.py | 32 + test/farm/html/run_styled.py | 28 + test/farm/html/run_tabbed.py | 24 + test/farm/html/run_unicode.py | 30 + test/farm/html/run_y_xml_branch.py | 21 + test/farm/html/src/a.py | 7 + test/farm/html/src/b.py | 29 + test/farm/html/src/bom.py | 11 + test/farm/html/src/coverage.xml | 20 + test/farm/html/src/extra.css | 1 + test/farm/html/src/here.py | 8 + test/farm/html/src/isolatin1.py | 5 + test/farm/html/src/m1.py | 2 + test/farm/html/src/m2.py | 2 + test/farm/html/src/m3.py | 2 + test/farm/html/src/main.py | 10 + test/farm/html/src/omit4.ini | 2 + test/farm/html/src/omit5.ini | 8 + test/farm/html/src/partial.py | 18 + test/farm/html/src/run_a_xml_2.ini | 3 + test/farm/html/src/tabbed.py | 7 + test/farm/html/src/unicode.py | 5 + test/farm/html/src/y.py | 9 + test/farm/run/run_chdir.py | 12 + test/farm/run/run_timid.py | 60 + test/farm/run/run_xxx.py | 12 + test/farm/run/src/chdir.py | 4 + test/farm/run/src/showtrace.py | 23 + test/farm/run/src/subdir/placeholder | 0 test/farm/run/src/xxx | 8 + test/js/index.html | 52 + test/js/tests.js | 204 +++ test/modules/aa/__init__.py | 1 + test/modules/aa/afile.odd.py | 1 + test/modules/aa/afile.py | 1 + test/modules/aa/bb.odd/bfile.py | 1 + test/modules/aa/bb/__init__.py | 1 + test/modules/aa/bb/bfile.odd.py | 1 + test/modules/aa/bb/bfile.py | 1 + test/modules/aa/bb/cc/__init__.py | 0 test/modules/aa/bb/cc/cfile.py | 1 + test/modules/aa/zfile.py | 1 + test/modules/covmod1.py | 3 + test/modules/pkg1/__init__.py | 3 + test/modules/pkg1/__main__.py | 3 + test/modules/pkg1/p1a.py | 5 + test/modules/pkg1/p1b.py | 3 + test/modules/pkg1/p1c.py | 3 + test/modules/pkg1/runmod2.py | 3 + test/modules/pkg1/sub/__init__.py | 0 test/modules/pkg1/sub/__main__.py | 3 + test/modules/pkg1/sub/ps1a.py | 3 + test/modules/pkg1/sub/runmod3.py | 3 + test/modules/pkg2/__init__.py | 2 + test/modules/pkg2/p2a.py | 3 + test/modules/pkg2/p2b.py | 3 + test/modules/runmod1.py | 3 + test/modules/usepkgs.py | 4 + test/moremodules/othermods/__init__.py | 0 test/moremodules/othermods/othera.py | 2 + test/moremodules/othermods/otherb.py | 2 + test/moremodules/othermods/sub/__init__.py | 0 test/moremodules/othermods/sub/osa.py | 2 + test/moremodules/othermods/sub/osb.py | 2 + test/osinfo.py | 71 + test/qunit/jquery.tmpl.min.js | 10 + test/qunit/qunit.css | 225 +++ test/qunit/qunit.js | 1448 ++++++++++++++++ test/stress_phystoken.tok | 52 + test/stress_phystoken_dos.tok | 52 + test/test_api.py | 571 +++++++ test/test_arcs.py | 571 +++++++ test/test_cmdline.py | 702 ++++++++ test/test_codeunit.py | 103 ++ test/test_config.py | 225 +++ test/test_coverage.py | 1730 ++++++++++++++++++++ test/test_data.py | 146 ++ test/test_execfile.py | 116 ++ test/test_farm.py | 366 +++++ test/test_files.py | 169 ++ test/test_html.py | 297 ++++ test/test_misc.py | 73 + test/test_oddball.py | 386 +++++ test/test_parser.py | 131 ++ test/test_phystokens.py | 79 + test/test_process.py | 575 +++++++ test/test_results.py | 60 + test/test_summary.py | 298 ++++ test/test_templite.py | 204 +++ test/test_testing.py | 192 +++ test/test_xml.py | 84 + test/try_execfile.py | 34 + test_old.sh | 8 + tox.ini | 32 + 239 files changed, 25996 insertions(+) create mode 100644 AUTHORS.txt create mode 100644 CHANGES.txt create mode 100644 MANIFEST.in create mode 100644 PKG-INFO create mode 100644 README.txt create mode 100644 coverage.egg-info/PKG-INFO create mode 100644 coverage.egg-info/SOURCES.txt create mode 100644 coverage.egg-info/dependency_links.txt create mode 100644 coverage.egg-info/entry_points.txt create mode 100644 coverage.egg-info/not-zip-safe create mode 100644 coverage.egg-info/top_level.txt create mode 100644 coverage/__init__.py create mode 100644 coverage/__main__.py create mode 100644 coverage/annotate.py create mode 100644 coverage/backward.py create mode 100644 coverage/bytecode.py create mode 100644 coverage/cmdline.py create mode 100644 coverage/codeunit.py create mode 100644 coverage/collector.py create mode 100644 coverage/config.py create mode 100644 coverage/control.py create mode 100644 coverage/data.py create mode 100644 coverage/execfile.py create mode 100644 coverage/files.py create mode 100644 coverage/fullcoverage/encodings.py create mode 100644 coverage/html.py create mode 100644 coverage/htmlfiles/coverage_html.js create mode 100644 coverage/htmlfiles/index.html create mode 100644 coverage/htmlfiles/jquery-1.4.3.min.js create mode 100644 coverage/htmlfiles/jquery.hotkeys.js create mode 100644 coverage/htmlfiles/jquery.isonscreen.js create mode 100644 coverage/htmlfiles/jquery.tablesorter.min.js create mode 100644 coverage/htmlfiles/keybd_closed.png create mode 100644 coverage/htmlfiles/keybd_open.png create mode 100644 coverage/htmlfiles/pyfile.html create mode 100644 coverage/htmlfiles/style.css create mode 100644 coverage/misc.py create mode 100644 coverage/parser.py create mode 100644 coverage/phystokens.py create mode 100644 coverage/report.py create mode 100644 coverage/results.py create mode 100644 coverage/summary.py create mode 100644 coverage/templite.py create mode 100644 coverage/tracer.c create mode 100644 coverage/version.py create mode 100644 coverage/xmlreport.py create mode 100644 doc/api.rst create mode 100644 doc/branch.rst create mode 100644 doc/changes.rst create mode 100644 doc/cmd.rst create mode 100644 doc/config.rst create mode 100644 doc/contributing.rst create mode 100644 doc/excluding.rst create mode 100644 doc/faq.rst create mode 100644 doc/index.rst create mode 100644 doc/install.rst create mode 100644 doc/source.rst create mode 100644 doc/subprocess.rst create mode 100644 doc/trouble.rst create mode 100644 igor.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test/__init__.py create mode 100644 test/backtest.py create mode 100644 test/backunittest.py create mode 100644 test/coveragetest.py create mode 100644 test/covmodzip1.py create mode 100644 test/eggsrc/egg1/__init__.py create mode 100644 test/eggsrc/egg1/egg1.py create mode 100644 test/eggsrc/setup.py create mode 100644 test/farm/annotate/annotate_dir.py create mode 100644 test/farm/annotate/gold/white.py,cover create mode 100644 test/farm/annotate/gold_anno_dir/a___init__.py,cover create mode 100644 test/farm/annotate/gold_anno_dir/a_a.py,cover create mode 100644 test/farm/annotate/gold_anno_dir/b___init__.py,cover create mode 100644 test/farm/annotate/gold_anno_dir/b_b.py,cover create mode 100644 test/farm/annotate/gold_anno_dir/multi.py,cover create mode 100644 test/farm/annotate/gold_multi/a/__init__.py,cover create mode 100644 test/farm/annotate/gold_multi/a/a.py,cover create mode 100644 test/farm/annotate/gold_multi/b/__init__.py,cover create mode 100644 test/farm/annotate/gold_multi/b/b.py,cover create mode 100644 test/farm/annotate/gold_multi/multi.py,cover create mode 100644 test/farm/annotate/gold_v24/white.py,cover create mode 100644 test/farm/annotate/run.py create mode 100644 test/farm/annotate/run_multi.py create mode 100644 test/farm/annotate/src/a/__init__.py create mode 100644 test/farm/annotate/src/a/a.py create mode 100644 test/farm/annotate/src/b/__init__.py create mode 100644 test/farm/annotate/src/b/b.py create mode 100644 test/farm/annotate/src/multi.py create mode 100644 test/farm/annotate/src/white.py create mode 100644 test/farm/html/gold_a/a.html create mode 100644 test/farm/html/gold_a/index.html create mode 100644 test/farm/html/gold_b_branch/b.html create mode 100644 test/farm/html/gold_b_branch/index.html create mode 100644 test/farm/html/gold_bom/bom.html create mode 100644 test/farm/html/gold_bom/index.html create mode 100644 test/farm/html/gold_isolatin1/index.html create mode 100644 test/farm/html/gold_isolatin1/isolatin1.html create mode 100644 test/farm/html/gold_omit_1/index.html create mode 100644 test/farm/html/gold_omit_1/m1.html create mode 100644 test/farm/html/gold_omit_1/m2.html create mode 100644 test/farm/html/gold_omit_1/m3.html create mode 100644 test/farm/html/gold_omit_1/main.html create mode 100644 test/farm/html/gold_omit_2/index.html create mode 100644 test/farm/html/gold_omit_2/m2.html create mode 100644 test/farm/html/gold_omit_2/m3.html create mode 100644 test/farm/html/gold_omit_2/main.html create mode 100644 test/farm/html/gold_omit_3/index.html create mode 100644 test/farm/html/gold_omit_3/m3.html create mode 100644 test/farm/html/gold_omit_3/main.html create mode 100644 test/farm/html/gold_omit_4/index.html create mode 100644 test/farm/html/gold_omit_4/m1.html create mode 100644 test/farm/html/gold_omit_4/m3.html create mode 100644 test/farm/html/gold_omit_4/main.html create mode 100644 test/farm/html/gold_omit_5/index.html create mode 100644 test/farm/html/gold_omit_5/m1.html create mode 100644 test/farm/html/gold_omit_5/main.html create mode 100644 test/farm/html/gold_other/blah_blah_other.html create mode 100644 test/farm/html/gold_other/here.html create mode 100644 test/farm/html/gold_other/index.html create mode 100644 test/farm/html/gold_partial/index.html create mode 100644 test/farm/html/gold_partial/partial.html create mode 100644 test/farm/html/gold_styled/a.html create mode 100644 test/farm/html/gold_styled/extra.css create mode 100644 test/farm/html/gold_styled/index.html create mode 100644 test/farm/html/gold_styled/style.css create mode 100644 test/farm/html/gold_unicode/index.html create mode 100644 test/farm/html/gold_unicode/unicode.html create mode 100644 test/farm/html/gold_x_xml/coverage.xml create mode 100644 test/farm/html/gold_y_xml_branch/coverage.xml create mode 100644 test/farm/html/othersrc/other.py create mode 100644 test/farm/html/run_a.py create mode 100644 test/farm/html/run_a_xml_1.py create mode 100644 test/farm/html/run_a_xml_2.py create mode 100644 test/farm/html/run_b_branch.py create mode 100644 test/farm/html/run_bom.py create mode 100644 test/farm/html/run_isolatin1.py create mode 100644 test/farm/html/run_omit_1.py create mode 100644 test/farm/html/run_omit_2.py create mode 100644 test/farm/html/run_omit_3.py create mode 100644 test/farm/html/run_omit_4.py create mode 100644 test/farm/html/run_omit_5.py create mode 100644 test/farm/html/run_other.py create mode 100644 test/farm/html/run_partial.py create mode 100644 test/farm/html/run_styled.py create mode 100644 test/farm/html/run_tabbed.py create mode 100644 test/farm/html/run_unicode.py create mode 100644 test/farm/html/run_y_xml_branch.py create mode 100644 test/farm/html/src/a.py create mode 100644 test/farm/html/src/b.py create mode 100644 test/farm/html/src/bom.py create mode 100644 test/farm/html/src/coverage.xml create mode 100644 test/farm/html/src/extra.css create mode 100644 test/farm/html/src/here.py create mode 100644 test/farm/html/src/isolatin1.py create mode 100644 test/farm/html/src/m1.py create mode 100644 test/farm/html/src/m2.py create mode 100644 test/farm/html/src/m3.py create mode 100644 test/farm/html/src/main.py create mode 100644 test/farm/html/src/omit4.ini create mode 100644 test/farm/html/src/omit5.ini create mode 100644 test/farm/html/src/partial.py create mode 100644 test/farm/html/src/run_a_xml_2.ini create mode 100644 test/farm/html/src/tabbed.py create mode 100644 test/farm/html/src/unicode.py create mode 100644 test/farm/html/src/y.py create mode 100644 test/farm/run/run_chdir.py create mode 100644 test/farm/run/run_timid.py create mode 100644 test/farm/run/run_xxx.py create mode 100644 test/farm/run/src/chdir.py create mode 100644 test/farm/run/src/showtrace.py create mode 100644 test/farm/run/src/subdir/placeholder create mode 100644 test/farm/run/src/xxx create mode 100644 test/js/index.html create mode 100644 test/js/tests.js create mode 100644 test/modules/aa/__init__.py create mode 100644 test/modules/aa/afile.odd.py create mode 100644 test/modules/aa/afile.py create mode 100644 test/modules/aa/bb.odd/bfile.py create mode 100644 test/modules/aa/bb/__init__.py create mode 100644 test/modules/aa/bb/bfile.odd.py create mode 100644 test/modules/aa/bb/bfile.py create mode 100644 test/modules/aa/bb/cc/__init__.py create mode 100644 test/modules/aa/bb/cc/cfile.py create mode 100644 test/modules/aa/zfile.py create mode 100644 test/modules/covmod1.py create mode 100644 test/modules/pkg1/__init__.py create mode 100644 test/modules/pkg1/__main__.py create mode 100644 test/modules/pkg1/p1a.py create mode 100644 test/modules/pkg1/p1b.py create mode 100644 test/modules/pkg1/p1c.py create mode 100644 test/modules/pkg1/runmod2.py create mode 100644 test/modules/pkg1/sub/__init__.py create mode 100644 test/modules/pkg1/sub/__main__.py create mode 100644 test/modules/pkg1/sub/ps1a.py create mode 100644 test/modules/pkg1/sub/runmod3.py create mode 100644 test/modules/pkg2/__init__.py create mode 100644 test/modules/pkg2/p2a.py create mode 100644 test/modules/pkg2/p2b.py create mode 100644 test/modules/runmod1.py create mode 100644 test/modules/usepkgs.py create mode 100644 test/moremodules/othermods/__init__.py create mode 100644 test/moremodules/othermods/othera.py create mode 100644 test/moremodules/othermods/otherb.py create mode 100644 test/moremodules/othermods/sub/__init__.py create mode 100644 test/moremodules/othermods/sub/osa.py create mode 100644 test/moremodules/othermods/sub/osb.py create mode 100644 test/osinfo.py create mode 100644 test/qunit/jquery.tmpl.min.js create mode 100644 test/qunit/qunit.css create mode 100644 test/qunit/qunit.js create mode 100644 test/stress_phystoken.tok create mode 100644 test/stress_phystoken_dos.tok create mode 100644 test/test_api.py create mode 100644 test/test_arcs.py create mode 100644 test/test_cmdline.py create mode 100644 test/test_codeunit.py create mode 100644 test/test_config.py create mode 100644 test/test_coverage.py create mode 100644 test/test_data.py create mode 100644 test/test_execfile.py create mode 100644 test/test_farm.py create mode 100644 test/test_files.py create mode 100644 test/test_html.py create mode 100644 test/test_misc.py create mode 100644 test/test_oddball.py create mode 100644 test/test_parser.py create mode 100644 test/test_phystokens.py create mode 100644 test/test_process.py create mode 100644 test/test_results.py create mode 100644 test/test_summary.py create mode 100644 test/test_templite.py create mode 100644 test/test_testing.py create mode 100644 test/test_xml.py create mode 100644 test/try_execfile.py create mode 100644 test_old.sh create mode 100644 tox.ini diff --git a/AUTHORS.txt b/AUTHORS.txt new file mode 100644 index 0000000..7fb80bd --- /dev/null +++ b/AUTHORS.txt @@ -0,0 +1,41 @@ +Coverage.py was originally written by Gareth Rees, and since 2004 has been +extended and maintained by Ned Batchelder. + +Other contributions have been made by: + +Marc Abramowitz +Chris Adams +Geoff Bache +Julian Berman +Titus Brown +Brett Cannon +Pablo Carballo +Guillaume Chazarain +David Christian +Marcus Cobden +Danek Duvall +Ben Finney +Martin Fuzzey +Imri Goldberg +Bill Hart +Christian Heimes +Devin Jeanpierre +Ross Lawley +Edward Loper +Sandra Martocchia +Patrick Mezard +Noel O'Boyle +Detlev Offenbach +JT Olds +George Paci +Catherine Proulx +Brandon Rhodes +Adi Roiban +Greg Rogers +George Song +David Stanek +Joseph Tate +Sigve Tjora +Mark van der Wal +Zooko Wilcox-O'Hearn +Christoph Zwerschke diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..9404fc0 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,943 @@ +------------------------------ +Change history for Coverage.py +------------------------------ + +Version 3.6 --- 5 January 2013 +------------------------------ + +- Added a page to the docs about troublesome situations, closing `issue 226`_, + and added some info to the TODO file, closing `issue 227`_. + +.. _issue 226: https://bitbucket.org/ned/coveragepy/issue/226/make-readme-section-to-describe-when +.. _issue 227: https://bitbucket.org/ned/coveragepy/issue/227/update-todo + + +Version 3.6b3 --- 29 December 2012 +---------------------------------- + +- Beta 2 broke the nose plugin. It's fixed again, closing `issue 224`_. + +.. _issue 224: https://bitbucket.org/ned/coveragepy/issue/224/36b2-breaks-nosexcover + + +Version 3.6b2 --- 23 December 2012 +---------------------------------- + +- Coverage.py runs on Python 2.3 and 2.4 again. It was broken in 3.6b1. + +- The C extension is optionally compiled using a different more widely-used + technique, taking another stab at fixing `issue 80`_ once and for all. + +- Combining data files would create entries for phantom files if used with + ``source`` and path aliases. It no longer does. + +- ``debug sys`` now shows the configuration file path that was read. + +- If an oddly-behaved package claims that code came from an empty-string + filename, coverage.py no longer associates it with the directory name, + fixing `issue 221`_. + +.. _issue 80: https://bitbucket.org/ned/coveragepy/issue/80/is-there-a-duck-typing-way-to-know-we-cant +.. _issue 221: https://bitbucket.org/ned/coveragepy/issue/221/coveragepy-incompatible-with-pyratemp + + +Version 3.6b1 --- 28 November 2012 +---------------------------------- + +- Wildcards in ``include=`` and ``omit=`` arguments were not handled properly + in reporting functions, though they were when running. Now they are handled + uniformly, closing `issue 143`_ and `issue 163`_. **NOTE**: it is possible + that your configurations may now be incorrect. If you use ``include`` or + ``omit`` during reporting, whether on the command line, through the API, or + in a configuration file, please check carefully that you were not relying on + the old broken behavior. + +- The **report**, **html**, and **xml** commands now accept a ``--fail-under`` + switch that indicates in the exit status whether the coverage percentage was + less than a particular value. Closes `issue 139`_. + +- The reporting functions coverage.report(), coverage.html_report(), and + coverage.xml_report() now all return a float, the total percentage covered + measurement. + +- The HTML report's title can now be set in the configuration file, with the + ``--title`` switch on the command line, or via the API. + +- Configuration files now support substitution of environment variables, using + syntax like ``${WORD}``. Closes `issue 97`_. + +- Embarrassingly, the `[xml] output=` setting in the .coveragerc file simply + didn't work. Now it does. + +- The XML report now consistently uses filenames for the filename attribute, + rather than sometimes using module names. Fixes `issue 67`_. + Thanks, Marcus Cobden. + +- Coverage percentage metrics are now computed slightly differently under + branch coverage. This means that completely unexecuted files will now + correctly have 0% coverage, fixing `issue 156`_. This also means that your + total coverage numbers will generally now be lower if you are measuring + branch coverage. + +- When installing, now in addition to creating a "coverage" command, two new + aliases are also installed. A "coverage2" or "coverage3" command will be + created, depending on whether you are installing in Python 2.x or 3.x. + A "coverage-X.Y" command will also be created corresponding to your specific + version of Python. Closes `issue 111`_. + +- The coverage.py installer no longer tries to bootstrap setuptools or + Distribute. You must have one of them installed first, as `issue 202`_ + recommended. + +- The coverage.py kit now includes docs (closing `issue 137`_) and tests. + +- On Windows, files are now reported in their correct case, fixing `issue 89`_ + and `issue 203`_. + +- If a file is missing during reporting, the path shown in the error message + is now correct, rather than an incorrect path in the current directory. + Fixes `issue 60`_. + +- Running an HTML report in Python 3 in the same directory as an old Python 2 + HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_) + is now fixed. + +- Fixed yet another error trying to parse non-Python files as Python, this + time an IndentationError, closing `issue 82`_ for the fourth time... + +- If `coverage xml` fails because there is no data to report, it used to + create a zero-length XML file. Now it doesn't, fixing `issue 210`_. + +- Jython files now work with the ``--source`` option, fixing `issue 100`_. + +- Running coverage under a debugger is unlikely to work, but it shouldn't fail + with "TypeError: 'NoneType' object is not iterable". Fixes `issue 201`_. + +- On some Linux distributions, when installed with the OS package manager, + coverage.py would report its own code as part of the results. Now it won't, + fixing `issue 214`_, though this will take some time to be repackaged by the + operating systems. + +- Docstrings for the legacy singleton methods are more helpful. Thanks Marius + Gedminas. Closes `issue 205`_. + +- The pydoc tool can now show docmentation for the class `coverage.coverage`. + Closes `issue 206`_. + +- Added a page to the docs about contributing to coverage.py, closing + `issue 171`_. + +- When coverage.py ended unsuccessfully, it may have reported odd errors like + ``'NoneType' object has no attribute 'isabs'``. It no longer does, + so kiss `issue 153`_ goodbye. + +.. _issue 60: https://bitbucket.org/ned/coveragepy/issue/60/incorrect-path-to-orphaned-pyc-files +.. _issue 67: https://bitbucket.org/ned/coveragepy/issue/67/xml-report-filenames-may-be-generated +.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _issue 89: https://bitbucket.org/ned/coveragepy/issue/89/on-windows-all-packages-are-reported-in +.. _issue 97: https://bitbucket.org/ned/coveragepy/issue/97/allow-environment-variables-to-be +.. _issue 100: https://bitbucket.org/ned/coveragepy/issue/100/source-directive-doesnt-work-for-packages +.. _issue 111: https://bitbucket.org/ned/coveragepy/issue/111/when-installing-coverage-with-pip-not +.. _issue 137: https://bitbucket.org/ned/coveragepy/issue/137/provide-docs-with-source-distribution +.. _issue 139: https://bitbucket.org/ned/coveragepy/issue/139/easy-check-for-a-certain-coverage-in-tests +.. _issue 143: https://bitbucket.org/ned/coveragepy/issue/143/omit-doesnt-seem-to-work-in-coverage +.. _issue 153: https://bitbucket.org/ned/coveragepy/issue/153/non-existent-filename-triggers +.. _issue 156: https://bitbucket.org/ned/coveragepy/issue/156/a-completely-unexecuted-file-shows-14 +.. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename +.. _issue 171: https://bitbucket.org/ned/coveragepy/issue/171/how-to-contribute-and-run-tests +.. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy +.. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on +.. _issue 202: https://bitbucket.org/ned/coveragepy/issue/202/get-rid-of-ez_setuppy-and +.. _issue 203: https://bitbucket.org/ned/coveragepy/issue/203/duplicate-filenames-reported-when-filename +.. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly +.. _issue 206: https://bitbucket.org/ned/coveragepy/issue/206/pydoc-coveragecoverage-fails-with-an-error +.. _issue 210: https://bitbucket.org/ned/coveragepy/issue/210/if-theres-no-coverage-data-coverage-xml +.. _issue 214: https://bitbucket.org/ned/coveragepy/issue/214/coveragepy-measures-itself-on-precise + + +Version 3.5.3 --- 29 September 2012 +----------------------------------- + +- Line numbers in the HTML report line up better with the source lines, fixing + `issue 197`_, thanks Marius Gedminas. + +- When specifying a directory as the source= option, the directory itself no + longer needs to have a ``__init__.py`` file, though its subdirectories do, to + be considered as source files. + +- Files encoded as UTF-8 with a BOM are now properly handled, fixing + `issue 179`_. Thanks, Pablo Carballo. + +- Fixed more cases of non-Python files being reported as Python source, and + then not being able to parse them as Python. Closes `issue 82`_ (again). + Thanks, Julian Berman. + +- Fixed memory leaks under Python 3, thanks, Brett Cannon. Closes `issue 147`_. + +- Optimized .pyo files may not have been handled correctly, `issue 195`_. + Thanks, Marius Gedminas. + +- Certain unusually named file paths could have been mangled during reporting, + `issue 194`_. Thanks, Marius Gedminas. + +- Try to do a better job of the impossible task of detecting when we can't + build the C extension, fixing `issue 183`_. + +- Testing is now done with `tox`_, thanks, Marc Abramowitz. + +.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _issue 147: https://bitbucket.org/ned/coveragepy/issue/147/massive-memory-usage-by-ctracer +.. _issue 179: https://bitbucket.org/ned/coveragepy/issue/179/htmlreporter-fails-when-source-file-is +.. _issue 183: https://bitbucket.org/ned/coveragepy/issue/183/install-fails-for-python-23 +.. _issue 194: https://bitbucket.org/ned/coveragepy/issue/194/filelocatorrelative_filename-could-mangle +.. _issue 195: https://bitbucket.org/ned/coveragepy/issue/195/pyo-file-handling-in-codeunit +.. _issue 197: https://bitbucket.org/ned/coveragepy/issue/197/line-numbers-in-html-report-do-not-align +.. _tox: http://tox.readthedocs.org/ + + + +Version 3.5.2 --- 4 May 2012 +---------------------------- + +No changes since 3.5.2.b1 + + +Version 3.5.2b1 --- 29 April 2012 +--------------------------------- + +- The HTML report has slightly tweaked controls: the buttons at the top of + the page are color-coded to the source lines they affect. + +- Custom CSS can be applied to the HTML report by specifying a CSS file as + the extra_css configuration value in the [html] section. + +- Source files with custom encodings declared in a comment at the top are now + properly handled during reporting on Python 2. Python 3 always handled them + properly. This fixes `issue 157`_. + +- Backup files left behind by editors are no longer collected by the source= + option, fixing `issue 168`_. + +- If a file doesn't parse properly as Python, we don't report it as an error + if the filename seems like maybe it wasn't meant to be Python. This is a + pragmatic fix for `issue 82`_. + +- The ``-m`` switch on ``coverage report``, which includes missing line numbers + in the summary report, can now be specifed as ``show_missing`` in the + config file. Closes `issue 173`_. + +- When running a module with ``coverage run -m ``, certain details + of the execution environment weren't the same as for + ``python -m ``. This had the unfortunate side-effect of making + ``coverage run -m unittest discover`` not work if you had tests in a + directory named "test". This fixes `issue 155`_. + +- Now the exit status of your product code is properly used as the process + status when running ``python -m coverage run ...``. Thanks, JT Olds. + +- When installing into pypy, we no longer attempt (and fail) to compile + the C tracer function, closing `issue 166`_. + +.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _issue 155: https://bitbucket.org/ned/coveragepy/issue/155/cant-use-coverage-run-m-unittest-discover +.. _issue 157: https://bitbucket.org/ned/coveragepy/issue/157/chokes-on-source-files-with-non-utf-8 +.. _issue 166: https://bitbucket.org/ned/coveragepy/issue/166/dont-try-to-compile-c-extension-on-pypy +.. _issue 168: https://bitbucket.org/ned/coveragepy/issue/168/dont-be-alarmed-by-emacs-droppings +.. _issue 173: https://bitbucket.org/ned/coveragepy/issue/173/theres-no-way-to-specify-show-missing-in + + +Version 3.5.1 --- 23 September 2011 +----------------------------------- + +- The ``[paths]`` feature unfortunately didn't work in real world situations + where you wanted to, you know, report on the combined data. Now all paths + stored in the combined file are canonicalized properly. + + +Version 3.5.1b1 --- 28 August 2011 +---------------------------------- + +- When combining data files from parallel runs, you can now instruct coverage + about which directories are equivalent on different machines. A ``[paths]`` + section in the configuration file lists paths that are to be considered + equivalent. Finishes `issue 17`_. + +- for-else constructs are understood better, and don't cause erroneous partial + branch warnings. Fixes `issue 122`_. + +- Branch coverage for ``with`` statements is improved, fixing `issue 128`_. + +- The number of partial branches reported on the HTML summary page was + different than the number reported on the individual file pages. This is + now fixed. + +- An explicit include directive to measure files in the Python installation + wouldn't work because of the standard library exclusion. Now the include + directive takes precendence, and the files will be measured. Fixes + `issue 138`_. + +- The HTML report now handles Unicode characters in Python source files + properly. This fixes `issue 124`_ and `issue 144`_. Thanks, Devin + Jeanpierre. + +- In order to help the core developers measure the test coverage of the + standard library, Brandon Rhodes devised an aggressive hack to trick Python + into running some coverage code before anything else in the process. + See the coverage/fullcoverage directory if you are interested. + +.. _issue 17: http://bitbucket.org/ned/coveragepy/issue/17/support-combining-coverage-data-from +.. _issue 122: http://bitbucket.org/ned/coveragepy/issue/122/for-else-always-reports-missing-branch +.. _issue 124: http://bitbucket.org/ned/coveragepy/issue/124/no-arbitrary-unicode-in-html-reports-in +.. _issue 128: http://bitbucket.org/ned/coveragepy/issue/128/branch-coverage-of-with-statement-in-27 +.. _issue 138: http://bitbucket.org/ned/coveragepy/issue/138/include-should-take-precedence-over-is +.. _issue 144: http://bitbucket.org/ned/coveragepy/issue/144/failure-generating-html-output-for + + +Version 3.5 --- 29 June 2011 +---------------------------- + +- The HTML report hotkeys now behave slightly differently when the current + chunk isn't visible at all: a chunk on the screen will be selected, + instead of the old behavior of jumping to the literal next chunk. + The hotkeys now work in Google Chrome. Thanks, Guido van Rossum. + + +Version 3.5b1 --- 5 June 2011 +----------------------------- + +- The HTML report now has hotkeys. Try ``n``, ``s``, ``m``, ``x``, ``b``, + ``p``, and ``c`` on the overview page to change the column sorting. + On a file page, ``r``, ``m``, ``x``, and ``p`` toggle the run, missing, + excluded, and partial line markings. You can navigate the highlighted + sections of code by using the ``j`` and ``k`` keys for next and previous. + The ``1`` (one) key jumps to the first highlighted section in the file, + and ``0`` (zero) scrolls to the top of the file. + +- The ``--omit`` and ``--include`` switches now interpret their values more + usefully. If the value starts with a wildcard character, it is used as-is. + If it does not, it is interpreted relative to the current directory. + Closes `issue 121`_. + +- Partial branch warnings can now be pragma'd away. The configuration option + ``partial_branches`` is a list of regular expressions. Lines matching any of + those expressions will never be marked as a partial branch. In addition, + there's a built-in list of regular expressions marking statements which should + never be marked as partial. This list includes ``while True:``, ``while 1:``, + ``if 1:``, and ``if 0:``. + +- The ``coverage()`` constructor accepts single strings for the ``omit=`` and + ``include=`` arguments, adapting to a common error in programmatic use. + +- Modules can now be run directly using ``coverage run -m modulename``, to + mirror Python's ``-m`` flag. Closes `issue 95`_, thanks, Brandon Rhodes. + +- ``coverage run`` didn't emulate Python accurately in one small detail: the + current directory inserted into ``sys.path`` was relative rather than + absolute. This is now fixed. + +- HTML reporting is now incremental: a record is kept of the data that + produced the HTML reports, and only files whose data has changed will + be generated. This should make most HTML reporting faster. + +- Pathological code execution could disable the trace function behind our + backs, leading to incorrect code measurement. Now if this happens, + coverage.py will issue a warning, at least alerting you to the problem. + Closes `issue 93`_. Thanks to Marius Gedminas for the idea. + +- The C-based trace function now behaves properly when saved and restored + with ``sys.gettrace()`` and ``sys.settrace()``. This fixes `issue 125`_ + and `issue 123`_. Thanks, Devin Jeanpierre. + +- Source files are now opened with Python 3.2's ``tokenize.open()`` where + possible, to get the best handling of Python source files with encodings. + Closes `issue 107`_, thanks, Brett Cannon. + +- Syntax errors in supposed Python files can now be ignored during reporting + with the ``-i`` switch just like other source errors. Closes `issue 115`_. + +- Installation from source now succeeds on machines without a C compiler, + closing `issue 80`_. + +- Coverage.py can now be run directly from a working tree by specifying + the directory name to python: ``python coverage_py_working_dir run ...``. + Thanks, Brett Cannon. + +- A little bit of Jython support: `coverage run` can now measure Jython + execution by adapting when $py.class files are traced. Thanks, Adi Roiban. + Jython still doesn't provide the Python libraries needed to make + coverage reporting work, unfortunately. + +- Internally, files are now closed explicitly, fixing `issue 104`_. Thanks, + Brett Cannon. + +.. _issue 80: https://bitbucket.org/ned/coveragepy/issue/80/is-there-a-duck-typing-way-to-know-we-cant +.. _issue 93: http://bitbucket.org/ned/coveragepy/issue/93/copying-a-mock-object-breaks-coverage +.. _issue 95: https://bitbucket.org/ned/coveragepy/issue/95/run-subcommand-should-take-a-module-name +.. _issue 104: https://bitbucket.org/ned/coveragepy/issue/104/explicitly-close-files +.. _issue 107: https://bitbucket.org/ned/coveragepy/issue/107/codeparser-not-opening-source-files-with +.. _issue 115: https://bitbucket.org/ned/coveragepy/issue/115/fail-gracefully-when-reporting-on-file +.. _issue 121: https://bitbucket.org/ned/coveragepy/issue/121/filename-patterns-are-applied-stupidly +.. _issue 123: https://bitbucket.org/ned/coveragepy/issue/123/pyeval_settrace-used-in-way-that-breaks +.. _issue 125: https://bitbucket.org/ned/coveragepy/issue/125/coverage-removes-decoratortoolss-tracing + + +Version 3.4 --- 19 September 2010 +--------------------------------- + +- The XML report is now sorted by package name, fixing `issue 88`_. + +- Programs that exited with ``sys.exit()`` with no argument weren't handled + properly, producing a coverage.py stack trace. That is now fixed. + +.. _issue 88: http://bitbucket.org/ned/coveragepy/issue/88/xml-report-lists-packages-in-random-order + + +Version 3.4b2 --- 6 September 2010 +---------------------------------- + +- Completely unexecuted files can now be included in coverage results, reported + as 0% covered. This only happens if the --source option is specified, since + coverage.py needs guidance about where to look for source files. + +- The XML report output now properly includes a percentage for branch coverage, + fixing `issue 65`_ and `issue 81`_. + +- Coverage percentages are now displayed uniformly across reporting methods. + Previously, different reports could round percentages differently. Also, + percentages are only reported as 0% or 100% if they are truly 0 or 100, and + are rounded otherwise. Fixes `issue 41`_ and `issue 70`_. + +- The precision of reported coverage percentages can be set with the + ``[report] precision`` config file setting. Completes `issue 16`_. + +- Threads derived from ``threading.Thread`` with an overridden `run` method + would report no coverage for the `run` method. This is now fixed, closing + `issue 85`_. + +.. _issue 16: http://bitbucket.org/ned/coveragepy/issue/16/allow-configuration-of-accuracy-of-percentage-totals +.. _issue 41: http://bitbucket.org/ned/coveragepy/issue/41/report-says-100-when-it-isnt-quite-there +.. _issue 65: http://bitbucket.org/ned/coveragepy/issue/65/branch-option-not-reported-in-cobertura +.. _issue 70: http://bitbucket.org/ned/coveragepy/issue/70/text-report-and-html-report-disagree-on-coverage +.. _issue 81: http://bitbucket.org/ned/coveragepy/issue/81/xml-report-does-not-have-condition-coverage-attribute-for-lines-with-a +.. _issue 85: http://bitbucket.org/ned/coveragepy/issue/85/threadrun-isnt-measured + + +Version 3.4b1 --- 21 August 2010 +-------------------------------- + +- BACKWARD INCOMPATIBILITY: the ``--omit`` and ``--include`` switches now take + file patterns rather than file prefixes, closing `issue 34`_ and `issue 36`_. + +- BACKWARD INCOMPATIBILITY: the `omit_prefixes` argument is gone throughout + coverage.py, replaced with `omit`, a list of filename patterns suitable for + `fnmatch`. A parallel argument `include` controls what files are included. + +- The run command now has a ``--source`` switch, a list of directories or + module names. If provided, coverage.py will only measure execution in those + source files. + +- Various warnings are printed to stderr for problems encountered during data + measurement: if a ``--source`` module has no Python source to measure, or is + never encountered at all, or if no data is collected. + +- The reporting commands (report, annotate, html, and xml) now have an + ``--include`` switch to restrict reporting to modules matching those file + patterns, similar to the existing ``--omit`` switch. Thanks, Zooko. + +- The run command now supports ``--include`` and ``--omit`` to control what + modules it measures. This can speed execution and reduce the amount of data + during reporting. Thanks Zooko. + +- Since coverage.py 3.1, using the Python trace function has been slower than + it needs to be. A cache of tracing decisions was broken, but has now been + fixed. + +- Python 2.7 and 3.2 have introduced new opcodes that are now supported. + +- Python files with no statements, for example, empty ``__init__.py`` files, + are now reported as having zero statements instead of one. Fixes `issue 1`_. + +- Reports now have a column of missed line counts rather than executed line + counts, since developers should focus on reducing the missed lines to zero, + rather than increasing the executed lines to varying targets. Once + suggested, this seemed blindingly obvious. + +- Line numbers in HTML source pages are clickable, linking directly to that + line, which is highlighted on arrival. Added a link back to the index page + at the bottom of each HTML page. + +- Programs that call ``os.fork`` will properly collect data from both the child + and parent processes. Use ``coverage run -p`` to get two data files that can + be combined with ``coverage combine``. Fixes `issue 56`_. + +- Coverage is now runnable as a module: ``python -m coverage``. Thanks, + Brett Cannon. + +- When measuring code running in a virtualenv, most of the system library was + being measured when it shouldn't have been. This is now fixed. + +- Doctest text files are no longer recorded in the coverage data, since they + can't be reported anyway. Fixes `issue 52`_ and `issue 61`_. + +- Jinja HTML templates compile into Python code using the HTML filename, + which confused coverage.py. Now these files are no longer traced, fixing + `issue 82`_. + +- Source files can have more than one dot in them (foo.test.py), and will be + treated properly while reporting. Fixes `issue 46`_. + +- Source files with DOS line endings are now properly tokenized for syntax + coloring on non-DOS machines. Fixes `issue 53`_. + +- Unusual code structure that confused exits from methods with exits from + classes is now properly analyzed. See `issue 62`_. + +- Asking for an HTML report with no files now shows a nice error message rather + than a cryptic failure ('int' object is unsubscriptable). Fixes `issue 59`_. + +.. _issue 1: http://bitbucket.org/ned/coveragepy/issue/1/empty-__init__py-files-are-reported-as-1-executable +.. _issue 34: http://bitbucket.org/ned/coveragepy/issue/34/enhanced-omit-globbing-handling +.. _issue 36: http://bitbucket.org/ned/coveragepy/issue/36/provide-regex-style-omit +.. _issue 46: http://bitbucket.org/ned/coveragepy/issue/46 +.. _issue 53: http://bitbucket.org/ned/coveragepy/issue/53 +.. _issue 52: http://bitbucket.org/ned/coveragepy/issue/52/doctesttestfile-confuses-source-detection +.. _issue 56: http://bitbucket.org/ned/coveragepy/issue/56 +.. _issue 61: http://bitbucket.org/ned/coveragepy/issue/61/annotate-i-doesnt-work +.. _issue 62: http://bitbucket.org/ned/coveragepy/issue/62 +.. _issue 59: http://bitbucket.org/ned/coveragepy/issue/59/html-report-fails-with-int-object-is +.. _issue 82: http://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report + + +Version 3.3.1 --- 6 March 2010 +------------------------------ + +- Using `parallel=True` in .coveragerc file prevented reporting, but now does + not, fixing `issue 49`_. + +- When running your code with "coverage run", if you call `sys.exit()`, + coverage.py will exit with that status code, fixing `issue 50`_. + +.. _issue 49: http://bitbucket.org/ned/coveragepy/issue/49 +.. _issue 50: http://bitbucket.org/ned/coveragepy/issue/50 + + +Version 3.3 --- 24 February 2010 +-------------------------------- + +- Settings are now read from a .coveragerc file. A specific file can be + specified on the command line with --rcfile=FILE. The name of the file can + be programmatically set with the `config_file` argument to the coverage() + constructor, or reading a config file can be disabled with + `config_file=False`. + +- Fixed a problem with nested loops having their branch possibilities + mischaracterized: `issue 39`_. + +- Added coverage.process_start to enable coverage measurement when Python + starts. + +- Parallel data file names now have a random number appended to them in + addition to the machine name and process id. + +- Parallel data files combined with "coverage combine" are deleted after + they're combined, to clean up unneeded files. Fixes `issue 40`_. + +- Exceptions thrown from product code run with "coverage run" are now displayed + without internal coverage.py frames, so the output is the same as when the + code is run without coverage.py. + +- The `data_suffix` argument to the coverage constructor is now appended with + an added dot rather than simply appended, so that .coveragerc files will not + be confused for data files. + +- Python source files that don't end with a newline can now be executed, fixing + `issue 47`_. + +- Added an AUTHORS.txt file. + +.. _issue 39: http://bitbucket.org/ned/coveragepy/issue/39 +.. _issue 40: http://bitbucket.org/ned/coveragepy/issue/40 +.. _issue 47: http://bitbucket.org/ned/coveragepy/issue/47 + + +Version 3.2 --- 5 December 2009 +------------------------------- + +- Added a ``--version`` option on the command line. + + +Version 3.2b4 --- 1 December 2009 +--------------------------------- + +- Branch coverage improvements: + + - The XML report now includes branch information. + +- Click-to-sort HTML report columns are now persisted in a cookie. Viewing + a report will sort it first the way you last had a coverage report sorted. + Thanks, `Chris Adams`_. + +- On Python 3.x, setuptools has been replaced by `Distribute`_. + +.. _Distribute: http://packages.python.org/distribute/ + + +Version 3.2b3 --- 23 November 2009 +---------------------------------- + +- Fixed a memory leak in the C tracer that was introduced in 3.2b1. + +- Branch coverage improvements: + + - Branches to excluded code are ignored. + +- The table of contents in the HTML report is now sortable: click the headers + on any column. Thanks, `Chris Adams`_. + +.. _Chris Adams: http://improbable.org/chris/ + + +Version 3.2b2 --- 19 November 2009 +---------------------------------- + +- Branch coverage improvements: + + - Classes are no longer incorrectly marked as branches: `issue 32`_. + + - "except" clauses with types are no longer incorrectly marked as branches: + `issue 35`_. + +- Fixed some problems syntax coloring sources with line continuations and + source with tabs: `issue 30`_ and `issue 31`_. + +- The --omit option now works much better than before, fixing `issue 14`_ and + `issue 33`_. Thanks, Danek Duvall. + +.. _issue 14: http://bitbucket.org/ned/coveragepy/issue/14 +.. _issue 30: http://bitbucket.org/ned/coveragepy/issue/30 +.. _issue 31: http://bitbucket.org/ned/coveragepy/issue/31 +.. _issue 32: http://bitbucket.org/ned/coveragepy/issue/32 +.. _issue 33: http://bitbucket.org/ned/coveragepy/issue/33 +.. _issue 35: http://bitbucket.org/ned/coveragepy/issue/35 + + +Version 3.2b1 --- 10 November 2009 +---------------------------------- + +- Branch coverage! + +- XML reporting has file paths that let Cobertura find the source code. + +- The tracer code has changed, it's a few percent faster. + +- Some exceptions reported by the command line interface have been cleaned up + so that tracebacks inside coverage.py aren't shown. Fixes `issue 23`_. + +.. _issue 23: http://bitbucket.org/ned/coveragepy/issue/23 + + +Version 3.1 --- 4 October 2009 +------------------------------ + +- Source code can now be read from eggs. Thanks, Ross Lawley. Fixes + `issue 25`_. + +.. _issue 25: http://bitbucket.org/ned/coveragepy/issue/25 + + +Version 3.1b1 --- 27 September 2009 +----------------------------------- + +- Python 3.1 is now supported. + +- Coverage.py has a new command line syntax with sub-commands. This expands + the possibilities for adding features and options in the future. The old + syntax is still supported. Try "coverage help" to see the new commands. + Thanks to Ben Finney for early help. + +- Added an experimental "coverage xml" command for producing coverage reports + in a Cobertura-compatible XML format. Thanks, Bill Hart. + +- Added the --timid option to enable a simpler slower trace function that works + for DecoratorTools projects, including TurboGears. Fixed `issue 12`_ and + `issue 13`_. + +- HTML reports show modules from other directories. Fixed `issue 11`_. + +- HTML reports now display syntax-colored Python source. + +- Programs that change directory will still write .coverage files in the + directory where execution started. Fixed `issue 24`_. + +- Added a "coverage debug" command for getting diagnostic information about the + coverage.py installation. + +.. _issue 11: http://bitbucket.org/ned/coveragepy/issue/11 +.. _issue 12: http://bitbucket.org/ned/coveragepy/issue/12 +.. _issue 13: http://bitbucket.org/ned/coveragepy/issue/13 +.. _issue 24: http://bitbucket.org/ned/coveragepy/issue/24 + + +Version 3.0.1 --- 7 July 2009 +----------------------------- + +- Removed the recursion limit in the tracer function. Previously, code that + ran more than 500 frames deep would crash. Fixed `issue 9`_. + +- Fixed a bizarre problem involving pyexpat, whereby lines following XML parser + invocations could be overlooked. Fixed `issue 10`_. + +- On Python 2.3, coverage.py could mis-measure code with exceptions being + raised. This is now fixed. + +- The coverage.py code itself will now not be measured by coverage.py, and no + coverage modules will be mentioned in the nose --with-cover plug-in. Fixed + `issue 8`_. + +- When running source files, coverage.py now opens them in universal newline + mode just like Python does. This lets it run Windows files on Mac, for + example. + +.. _issue 9: http://bitbucket.org/ned/coveragepy/issue/9 +.. _issue 10: http://bitbucket.org/ned/coveragepy/issue/10 +.. _issue 8: http://bitbucket.org/ned/coveragepy/issue/8 + + +Version 3.0 --- 13 June 2009 +---------------------------- + +- Fixed the way the Python library was ignored. Too much code was being + excluded the old way. + +- Tabs are now properly converted in HTML reports. Previously indentation was + lost. Fixed `issue 6`_. + +- Nested modules now get a proper flat_rootname. Thanks, Christian Heimes. + +.. _issue 6: http://bitbucket.org/ned/coveragepy/issue/6 + + +Version 3.0b3 --- 16 May 2009 +----------------------------- + +- Added parameters to coverage.__init__ for options that had been set on the + coverage object itself. + +- Added clear_exclude() and get_exclude_list() methods for programmatic + manipulation of the exclude regexes. + +- Added coverage.load() to read previously-saved data from the data file. + +- Improved the finding of code files. For example, .pyc files that have been + installed after compiling are now located correctly. Thanks, Detlev + Offenbach. + +- When using the object API (that is, constructing a coverage() object), data + is no longer saved automatically on process exit. You can re-enable it with + the auto_data=True parameter on the coverage() constructor. The module-level + interface still uses automatic saving. + + +Version 3.0b --- 30 April 2009 +------------------------------ + +HTML reporting, and continued refactoring. + +- HTML reports and annotation of source files: use the new -b (browser) switch. + Thanks to George Song for code, inspiration and guidance. + +- Code in the Python standard library is not measured by default. If you need + to measure standard library code, use the -L command-line switch during + execution, or the cover_pylib=True argument to the coverage() constructor. + +- Source annotation into a directory (-a -d) behaves differently. The + annotated files are named with their hierarchy flattened so that same-named + files from different directories no longer collide. Also, only files in the + current tree are included. + +- coverage.annotate_file is no longer available. + +- Programs executed with -x now behave more as they should, for example, + __file__ has the correct value. + +- .coverage data files have a new pickle-based format designed for better + extensibility. + +- Removed the undocumented cache_file argument to coverage.usecache(). + + +Version 3.0b1 --- 7 March 2009 +------------------------------ + +Major overhaul. + +- Coverage is now a package rather than a module. Functionality has been split + into classes. + +- The trace function is implemented in C for speed. Coverage runs are now + much faster. Thanks to David Christian for productive micro-sprints and + other encouragement. + +- Executable lines are identified by reading the line number tables in the + compiled code, removing a great deal of complicated analysis code. + +- Precisely which lines are considered executable has changed in some cases. + Therefore, your coverage stats may also change slightly. + +- 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. + +- The minimum supported Python version is 2.3. + + +Version 2.85 --- 14 September 2008 +---------------------------------- + +- Add support for finding source files in eggs. Don't check for + morf's being instances of ModuleType, instead use duck typing so that + pseudo-modules can participate. Thanks, Imri Goldberg. + +- Use os.realpath as part of the fixing of file names so that symlinks won't + confuse things. Thanks, Patrick Mezard. + + +Version 2.80 --- 25 May 2008 +---------------------------- + +- Open files in rU mode to avoid line ending craziness. Thanks, Edward Loper. + + +Version 2.78 --- 30 September 2007 +---------------------------------- + +- Don't try to predict whether a file is Python source based on the extension. + Extension-less files are often Pythons scripts. Instead, simply parse the file + and catch the syntax errors. Hat tip to Ben Finney. + + +Version 2.77 --- 29 July 2007 +----------------------------- + +- Better packaging. + + +Version 2.76 --- 23 July 2007 +----------------------------- + +- Now Python 2.5 is *really* fully supported: the body of the new with + statement is counted as executable. + + +Version 2.75 --- 22 July 2007 +----------------------------- + +- Python 2.5 now fully supported. The method of dealing with multi-line + statements is now less sensitive to the exact line that Python reports during + execution. Pass statements are handled specially so that their disappearance + during execution won't throw off the measurement. + + +Version 2.7 --- 21 July 2007 +---------------------------- + +- "#pragma: nocover" is excluded by default. + +- Properly ignore docstrings and other constant expressions that appear in the + middle of a function, a problem reported by Tim Leslie. + +- coverage.erase() shouldn't clobber the exclude regex. Change how parallel + mode is invoked, and fix erase() so that it erases the cache when called + programmatically. + +- In reports, ignore code executed from strings, since we can't do anything + useful with it anyway. + +- Better file handling on Linux, thanks Guillaume Chazarain. + +- Better shell support on Windows, thanks Noel O'Boyle. + +- Python 2.2 support maintained, thanks Catherine Proulx. + +- Minor changes to avoid lint warnings. + + +Version 2.6 --- 23 August 2006 +------------------------------ + +- Applied Joseph Tate's patch for function decorators. + +- Applied Sigve Tjora and Mark van der Wal's fixes for argument handling. + +- Applied Geoff Bache's parallel mode patch. + +- Refactorings to improve testability. Fixes to command-line logic for parallel + mode and collect. + + +Version 2.5 --- 4 December 2005 +------------------------------- + +- Call threading.settrace so that all threads are measured. Thanks Martin + Fuzzey. + +- Add a file argument to report so that reports can be captured to a different + destination. + +- coverage.py can now measure itself. + +- Adapted Greg Rogers' patch for using relative file names, and sorting and + omitting files to report on. + + +Version 2.2 --- 31 December 2004 +-------------------------------- + +- Allow for keyword arguments in the module global functions. Thanks, Allen. + + +Version 2.1 --- 14 December 2004 +-------------------------------- + +- Return 'analysis' to its original behavior and add 'analysis2'. Add a global + for 'annotate', and factor it, adding 'annotate_file'. + + +Version 2.0 --- 12 December 2004 +-------------------------------- + +Significant code changes. + +- Finding executable statements has been rewritten so that docstrings and + other quirks of Python execution aren't mistakenly identified as missing + lines. + +- Lines can be excluded from consideration, even entire suites of lines. + +- The file system cache of covered lines can be disabled programmatically. + +- Modernized the code. + + +Earlier History +--------------- + +2001-12-04 GDR Created. + +2001-12-06 GDR Added command-line interface and source code annotation. + +2001-12-09 GDR Moved design and interface to separate documents. + +2001-12-10 GDR Open cache file as binary on Windows. Allow simultaneous -e and +-x, or -a and -r. + +2001-12-12 GDR Added command-line help. Cache analysis so that it only needs to +be done once when you specify -a and -r. + +2001-12-13 GDR Improved speed while recording. Portable between Python 1.5.2 +and 2.1.1. + +2002-01-03 GDR Module-level functions work correctly. + +2002-01-07 GDR Update sys.path when running a file with the -x option, so that +it matches the value the program would get if it were run on its own. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..0150d90 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,16 @@ +# MANIFEST.in file for coverage.py +recursive-include coverage/htmlfiles * +recursive-include coverage/fullcoverage * + +include coverage.egg-info/*.* +include setup.py +include README.txt +include CHANGES.txt +include AUTHORS.txt +include requirements.txt +include igor.py +include tox.ini + +recursive-include test * +recursive-include doc *.rst +global-exclude *.pyc diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..0bfedd5 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,38 @@ +Metadata-Version: 1.1 +Name: coverage +Version: 3.6 +Summary: Code coverage measurement for Python +Home-page: http://nedbatchelder.com/code/coverage +Author: Ned Batchelder and others +Author-email: ned@nedbatchelder.com +License: BSD +Description: Coverage.py measures code coverage, typically during test execution. It uses + the code analysis tools and tracing hooks provided in the Python standard + library to determine which lines are executable, and which have been executed. + + Coverage.py runs on Pythons 2.3 through 3.3, and PyPy 1.9. + + Documentation is at `nedbatchelder.com `_. Code repository and issue + tracker are at `bitbucket.org `_. + + New in 3.6: ``--fail-under``, and >20 bugs closed. + + New in 3.5: Branch coverage exclusions, keyboard shortcuts in HTML report. + + New in 3.4: Better control over source to measure, and unexecuted files + can be reported. + + New in 3.3: .coveragerc files. + + New in 3.2: Branch coverage! +Keywords: code coverage testing +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Software Development :: Quality Assurance +Classifier: Topic :: Software Development :: Testing +Classifier: Development Status :: 5 - Production/Stable diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..2c6fe3d --- /dev/null +++ b/README.txt @@ -0,0 +1,9 @@ +Coverage.py: code coverage testing for Python + +Coverage.py measures code coverage, typically during test execution. It uses +the code analysis tools and tracing hooks provided in the Python standard +library to determine which lines are executable, and which have been executed. + +For more information, see http://nedbatchelder.com/code/coverage + +Code repo and issue tracking are at http://bitbucket.org/ned/coveragepy diff --git a/coverage.egg-info/PKG-INFO b/coverage.egg-info/PKG-INFO new file mode 100644 index 0000000..0bfedd5 --- /dev/null +++ b/coverage.egg-info/PKG-INFO @@ -0,0 +1,38 @@ +Metadata-Version: 1.1 +Name: coverage +Version: 3.6 +Summary: Code coverage measurement for Python +Home-page: http://nedbatchelder.com/code/coverage +Author: Ned Batchelder and others +Author-email: ned@nedbatchelder.com +License: BSD +Description: Coverage.py measures code coverage, typically during test execution. It uses + the code analysis tools and tracing hooks provided in the Python standard + library to determine which lines are executable, and which have been executed. + + Coverage.py runs on Pythons 2.3 through 3.3, and PyPy 1.9. + + Documentation is at `nedbatchelder.com `_. Code repository and issue + tracker are at `bitbucket.org `_. + + New in 3.6: ``--fail-under``, and >20 bugs closed. + + New in 3.5: Branch coverage exclusions, keyboard shortcuts in HTML report. + + New in 3.4: Better control over source to measure, and unexecuted files + can be reported. + + New in 3.3: .coveragerc files. + + New in 3.2: Branch coverage! +Keywords: code coverage testing +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Software Development :: Quality Assurance +Classifier: Topic :: Software Development :: Testing +Classifier: Development Status :: 5 - Production/Stable diff --git a/coverage.egg-info/SOURCES.txt b/coverage.egg-info/SOURCES.txt new file mode 100644 index 0000000..9810573 --- /dev/null +++ b/coverage.egg-info/SOURCES.txt @@ -0,0 +1,237 @@ +AUTHORS.txt +CHANGES.txt +MANIFEST.in +README.txt +igor.py +requirements.txt +setup.py +test_old.sh +tox.ini +coverage/__init__.py +coverage/__main__.py +coverage/annotate.py +coverage/backward.py +coverage/bytecode.py +coverage/cmdline.py +coverage/codeunit.py +coverage/collector.py +coverage/config.py +coverage/control.py +coverage/data.py +coverage/execfile.py +coverage/files.py +coverage/html.py +coverage/misc.py +coverage/parser.py +coverage/phystokens.py +coverage/report.py +coverage/results.py +coverage/summary.py +coverage/templite.py +coverage/tracer.c +coverage/version.py +coverage/xmlreport.py +coverage.egg-info/PKG-INFO +coverage.egg-info/SOURCES.txt +coverage.egg-info/dependency_links.txt +coverage.egg-info/entry_points.txt +coverage.egg-info/not-zip-safe +coverage.egg-info/top_level.txt +coverage/fullcoverage/encodings.py +coverage/htmlfiles/coverage_html.js +coverage/htmlfiles/index.html +coverage/htmlfiles/jquery-1.4.3.min.js +coverage/htmlfiles/jquery.hotkeys.js +coverage/htmlfiles/jquery.isonscreen.js +coverage/htmlfiles/jquery.tablesorter.min.js +coverage/htmlfiles/keybd_closed.png +coverage/htmlfiles/keybd_open.png +coverage/htmlfiles/pyfile.html +coverage/htmlfiles/style.css +doc/api.rst +doc/branch.rst +doc/changes.rst +doc/cmd.rst +doc/config.rst +doc/contributing.rst +doc/excluding.rst +doc/faq.rst +doc/index.rst +doc/install.rst +doc/source.rst +doc/subprocess.rst +doc/trouble.rst +test/__init__.py +test/backtest.py +test/backunittest.py +test/coveragetest.py +test/covmodzip1.py +test/osinfo.py +test/stress_phystoken.tok +test/stress_phystoken_dos.tok +test/test_api.py +test/test_arcs.py +test/test_cmdline.py +test/test_codeunit.py +test/test_config.py +test/test_coverage.py +test/test_data.py +test/test_execfile.py +test/test_farm.py +test/test_files.py +test/test_html.py +test/test_misc.py +test/test_oddball.py +test/test_parser.py +test/test_phystokens.py +test/test_process.py +test/test_results.py +test/test_summary.py +test/test_templite.py +test/test_testing.py +test/test_xml.py +test/try_execfile.py +test/eggsrc/setup.py +test/eggsrc/egg1/__init__.py +test/eggsrc/egg1/egg1.py +test/farm/annotate/annotate_dir.py +test/farm/annotate/run.py +test/farm/annotate/run_multi.py +test/farm/annotate/gold/white.py,cover +test/farm/annotate/gold_anno_dir/a___init__.py,cover +test/farm/annotate/gold_anno_dir/a_a.py,cover +test/farm/annotate/gold_anno_dir/b___init__.py,cover +test/farm/annotate/gold_anno_dir/b_b.py,cover +test/farm/annotate/gold_anno_dir/multi.py,cover +test/farm/annotate/gold_multi/multi.py,cover +test/farm/annotate/gold_multi/a/__init__.py,cover +test/farm/annotate/gold_multi/a/a.py,cover +test/farm/annotate/gold_multi/b/__init__.py,cover +test/farm/annotate/gold_multi/b/b.py,cover +test/farm/annotate/gold_v24/white.py,cover +test/farm/annotate/src/multi.py +test/farm/annotate/src/white.py +test/farm/annotate/src/a/__init__.py +test/farm/annotate/src/a/a.py +test/farm/annotate/src/b/__init__.py +test/farm/annotate/src/b/b.py +test/farm/html/run_a.py +test/farm/html/run_a_xml_1.py +test/farm/html/run_a_xml_2.py +test/farm/html/run_b_branch.py +test/farm/html/run_bom.py +test/farm/html/run_isolatin1.py +test/farm/html/run_omit_1.py +test/farm/html/run_omit_2.py +test/farm/html/run_omit_3.py +test/farm/html/run_omit_4.py +test/farm/html/run_omit_5.py +test/farm/html/run_other.py +test/farm/html/run_partial.py +test/farm/html/run_styled.py +test/farm/html/run_tabbed.py +test/farm/html/run_unicode.py +test/farm/html/run_y_xml_branch.py +test/farm/html/gold_a/a.html +test/farm/html/gold_a/index.html +test/farm/html/gold_b_branch/b.html +test/farm/html/gold_b_branch/index.html +test/farm/html/gold_bom/bom.html +test/farm/html/gold_bom/index.html +test/farm/html/gold_isolatin1/index.html +test/farm/html/gold_isolatin1/isolatin1.html +test/farm/html/gold_omit_1/index.html +test/farm/html/gold_omit_1/m1.html +test/farm/html/gold_omit_1/m2.html +test/farm/html/gold_omit_1/m3.html +test/farm/html/gold_omit_1/main.html +test/farm/html/gold_omit_2/index.html +test/farm/html/gold_omit_2/m2.html +test/farm/html/gold_omit_2/m3.html +test/farm/html/gold_omit_2/main.html +test/farm/html/gold_omit_3/index.html +test/farm/html/gold_omit_3/m3.html +test/farm/html/gold_omit_3/main.html +test/farm/html/gold_omit_4/index.html +test/farm/html/gold_omit_4/m1.html +test/farm/html/gold_omit_4/m3.html +test/farm/html/gold_omit_4/main.html +test/farm/html/gold_omit_5/index.html +test/farm/html/gold_omit_5/m1.html +test/farm/html/gold_omit_5/main.html +test/farm/html/gold_other/blah_blah_other.html +test/farm/html/gold_other/here.html +test/farm/html/gold_other/index.html +test/farm/html/gold_partial/index.html +test/farm/html/gold_partial/partial.html +test/farm/html/gold_styled/a.html +test/farm/html/gold_styled/extra.css +test/farm/html/gold_styled/index.html +test/farm/html/gold_styled/style.css +test/farm/html/gold_unicode/index.html +test/farm/html/gold_unicode/unicode.html +test/farm/html/gold_x_xml/coverage.xml +test/farm/html/gold_y_xml_branch/coverage.xml +test/farm/html/othersrc/other.py +test/farm/html/src/a.py +test/farm/html/src/b.py +test/farm/html/src/bom.py +test/farm/html/src/coverage.xml +test/farm/html/src/extra.css +test/farm/html/src/here.py +test/farm/html/src/isolatin1.py +test/farm/html/src/m1.py +test/farm/html/src/m2.py +test/farm/html/src/m3.py +test/farm/html/src/main.py +test/farm/html/src/omit4.ini +test/farm/html/src/omit5.ini +test/farm/html/src/partial.py +test/farm/html/src/run_a_xml_2.ini +test/farm/html/src/tabbed.py +test/farm/html/src/unicode.py +test/farm/html/src/y.py +test/farm/run/run_chdir.py +test/farm/run/run_timid.py +test/farm/run/run_xxx.py +test/farm/run/src/chdir.py +test/farm/run/src/showtrace.py +test/farm/run/src/xxx +test/farm/run/src/subdir/placeholder +test/js/index.html +test/js/tests.js +test/modules/covmod1.py +test/modules/runmod1.py +test/modules/usepkgs.py +test/modules/aa/__init__.py +test/modules/aa/afile.odd.py +test/modules/aa/afile.py +test/modules/aa/zfile.py +test/modules/aa/bb/__init__.py +test/modules/aa/bb/bfile.odd.py +test/modules/aa/bb/bfile.py +test/modules/aa/bb.odd/bfile.py +test/modules/aa/bb/cc/__init__.py +test/modules/aa/bb/cc/cfile.py +test/modules/pkg1/__init__.py +test/modules/pkg1/__main__.py +test/modules/pkg1/p1a.py +test/modules/pkg1/p1b.py +test/modules/pkg1/p1c.py +test/modules/pkg1/runmod2.py +test/modules/pkg1/sub/__init__.py +test/modules/pkg1/sub/__main__.py +test/modules/pkg1/sub/ps1a.py +test/modules/pkg1/sub/runmod3.py +test/modules/pkg2/__init__.py +test/modules/pkg2/p2a.py +test/modules/pkg2/p2b.py +test/moremodules/othermods/__init__.py +test/moremodules/othermods/othera.py +test/moremodules/othermods/otherb.py +test/moremodules/othermods/sub/__init__.py +test/moremodules/othermods/sub/osa.py +test/moremodules/othermods/sub/osb.py +test/qunit/jquery.tmpl.min.js +test/qunit/qunit.css +test/qunit/qunit.js \ No newline at end of file diff --git a/coverage.egg-info/dependency_links.txt b/coverage.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/coverage.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/coverage.egg-info/entry_points.txt b/coverage.egg-info/entry_points.txt new file mode 100644 index 0000000..90d812f --- /dev/null +++ b/coverage.egg-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +coverage2 = coverage:main +coverage-2.7 = coverage:main +coverage = coverage:main + diff --git a/coverage.egg-info/not-zip-safe b/coverage.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/coverage.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/coverage.egg-info/top_level.txt b/coverage.egg-info/top_level.txt new file mode 100644 index 0000000..4ebc8ae --- /dev/null +++ b/coverage.egg-info/top_level.txt @@ -0,0 +1 @@ +coverage diff --git a/coverage/__init__.py b/coverage/__init__.py new file mode 100644 index 0000000..0ccc699 --- /dev/null +++ b/coverage/__init__.py @@ -0,0 +1,120 @@ +"""Code coverage measurement for Python. + +Ned Batchelder +http://nedbatchelder.com/code/coverage + +""" + +from coverage.version import __version__, __url__ + +from coverage.control import coverage, process_startup +from coverage.data import CoverageData +from coverage.cmdline import main, CoverageScript +from coverage.misc import CoverageException + +# Module-level functions. The original API to this module was based on +# functions defined directly in the module, with a singleton of the coverage() +# class. That design hampered programmability, so the current api uses +# explicitly-created coverage objects. But for backward compatibility, here we +# define the top-level functions to create the singleton when they are first +# called. + +# Singleton object for use with module-level functions. The singleton is +# created as needed when one of the module-level functions is called. +_the_coverage = None + +def _singleton_method(name): + """Return a function to the `name` method on a singleton `coverage` object. + + The singleton object is created the first time one of these functions is + called. + + """ + # Disable pylint msg W0612, because a bunch of variables look unused, but + # they're accessed via locals(). + # pylint: disable=W0612 + + def wrapper(*args, **kwargs): + """Singleton wrapper around a coverage method.""" + global _the_coverage + if not _the_coverage: + _the_coverage = coverage(auto_data=True) + return getattr(_the_coverage, name)(*args, **kwargs) + + import inspect + meth = getattr(coverage, name) + args, varargs, kw, defaults = inspect.getargspec(meth) + argspec = inspect.formatargspec(args[1:], varargs, kw, defaults) + docstring = meth.__doc__ + wrapper.__doc__ = ("""\ + A first-use-singleton wrapper around coverage.%(name)s. + + This wrapper is provided for backward compatibility with legacy code. + New code should use coverage.%(name)s directly. + + %(name)s%(argspec)s: + + %(docstring)s + """ % locals() + ) + + return wrapper + + +# Define the module-level functions. +use_cache = _singleton_method('use_cache') +start = _singleton_method('start') +stop = _singleton_method('stop') +erase = _singleton_method('erase') +exclude = _singleton_method('exclude') +analysis = _singleton_method('analysis') +analysis2 = _singleton_method('analysis2') +report = _singleton_method('report') +annotate = _singleton_method('annotate') + + +# 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 + +# 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: + pass + + +# COPYRIGHT AND LICENSE +# +# Copyright 2001 Gareth Rees. All rights reserved. +# Copyright 2004-2012 Ned Batchelder. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the +# distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. diff --git a/coverage/__main__.py b/coverage/__main__.py new file mode 100644 index 0000000..55e0d25 --- /dev/null +++ b/coverage/__main__.py @@ -0,0 +1,4 @@ +"""Coverage.py's main entry point.""" +import sys +from coverage.cmdline import main +sys.exit(main()) diff --git a/coverage/annotate.py b/coverage/annotate.py new file mode 100644 index 0000000..b7f32c1 --- /dev/null +++ b/coverage/annotate.py @@ -0,0 +1,101 @@ +"""Source file annotation for Coverage.""" + +import os, re + +from coverage.report import Reporter + +class AnnotateReporter(Reporter): + """Generate annotated source files showing line coverage. + + This reporter creates annotated copies of the measured source files. Each + .py file is copied as a .py,cover file, with a left-hand margin annotating + each line:: + + > def h(x): + - if 0: #pragma: no cover + - pass + > if x == 1: + ! a = 1 + > else: + > a = 2 + + > h(2) + + Executed lines use '>', lines not executed use '!', lines excluded from + consideration use '-'. + + """ + + def __init__(self, coverage, config): + super(AnnotateReporter, self).__init__(coverage, config) + self.directory = None + + blank_re = re.compile(r"\s*(#|$)") + else_re = re.compile(r"\s*else\s*:\s*(#|$)") + + def report(self, morfs, directory=None): + """Run the report. + + See `coverage.report()` for arguments. + + """ + self.report_files(self.annotate_file, morfs, directory) + + def annotate_file(self, cu, analysis): + """Annotate a single file. + + `cu` is the CodeUnit for the file to annotate. + + """ + if not cu.relative: + return + + filename = cu.filename + source = cu.source_file() + if self.directory: + dest_file = os.path.join(self.directory, cu.flat_rootname()) + dest_file += ".py,cover" + else: + dest_file = filename + ",cover" + dest = open(dest_file, 'w') + + statements = analysis.statements + missing = analysis.missing + excluded = analysis.excluded + + lineno = 0 + i = 0 + j = 0 + covered = True + while True: + line = source.readline() + if line == '': + break + lineno += 1 + while i < len(statements) and statements[i] < lineno: + i += 1 + while j < len(missing) and missing[j] < lineno: + j += 1 + if i < len(statements) and statements[i] == lineno: + covered = j >= len(missing) or missing[j] > lineno + if self.blank_re.match(line): + dest.write(' ') + elif self.else_re.match(line): + # Special logic for lines containing only 'else:'. + if i >= len(statements) and j >= len(missing): + dest.write('! ') + elif i >= len(statements) or j >= len(missing): + dest.write('> ') + elif statements[i] == missing[j]: + dest.write('! ') + else: + dest.write('> ') + elif lineno in excluded: + dest.write('- ') + elif covered: + dest.write('> ') + else: + dest.write('! ') + dest.write(line) + source.close() + dest.close() diff --git a/coverage/backward.py b/coverage/backward.py new file mode 100644 index 0000000..2c015af --- /dev/null +++ b/coverage/backward.py @@ -0,0 +1,147 @@ +"""Add things to old Pythons so I can pretend they are newer.""" + +# This file does lots of tricky stuff, so disable a bunch of lintisms. +# pylint: disable=F0401,W0611,W0622 +# F0401: Unable to import blah +# W0611: Unused import blah +# W0622: Redefining built-in blah + +import os, re, sys + +# Python 2.3 doesn't have `set` +try: + set = set # new in 2.4 +except NameError: + from sets import Set as set + +# Python 2.3 doesn't have `sorted`. +try: + sorted = sorted +except NameError: + def sorted(iterable): + """A 2.3-compatible implementation of `sorted`.""" + lst = list(iterable) + lst.sort() + return lst + +# rpartition is new in 2.5 +try: + "".rpartition +except AttributeError: + def rpartition(s, sep): + """Implement s.rpartition(sep) for old Pythons.""" + i = s.rfind(sep) + if i == -1: + return ('', '', s) + else: + return (s[:i], sep, s[i+len(sep):]) +else: + def rpartition(s, sep): + """A common interface for new Pythons.""" + return s.rpartition(sep) + +# Pythons 2 and 3 differ on where to get StringIO +try: + from cStringIO import StringIO + BytesIO = StringIO +except ImportError: + from io import StringIO, BytesIO + +# What's a string called? +try: + string_class = basestring +except NameError: + string_class = str + +# Where do pickles come from? +try: + import cPickle as pickle +except ImportError: + import pickle + +# range or xrange? +try: + range = xrange +except NameError: + range = range + +# A function to iterate listlessly over a dict's items. +if "iteritems" in dir({}): + def iitems(d): + """Produce the items from dict `d`.""" + return d.iteritems() +else: + def iitems(d): + """Produce the items from dict `d`.""" + return d.items() + +# Exec is a statement in Py2, a function in Py3 +if sys.version_info >= (3, 0): + def exec_code_object(code, global_map): + """A wrapper around exec().""" + exec(code, global_map) +else: + # OK, this is pretty gross. In Py2, exec was a statement, but that will + # be a syntax error if we try to put it in a Py3 file, even if it is never + # executed. So hide it inside an evaluated string literal instead. + eval( + compile( + "def exec_code_object(code, global_map):\n" + " exec code in global_map\n", + "", "exec" + ) + ) + +# Reading Python source and interpreting the coding comment is a big deal. +if sys.version_info >= (3, 0): + # Python 3.2 provides `tokenize.open`, the best way to open source files. + import tokenize + try: + open_source = tokenize.open # pylint: disable=E1101 + except AttributeError: + from io import TextIOWrapper + detect_encoding = tokenize.detect_encoding # pylint: disable=E1101 + # Copied from the 3.2 stdlib: + def open_source(fname): + """Open a file in read only mode using the encoding detected by + detect_encoding(). + """ + buffer = open(fname, 'rb') + encoding, _ = detect_encoding(buffer.readline) + buffer.seek(0) + text = TextIOWrapper(buffer, encoding, line_buffering=True) + text.mode = 'r' + return text +else: + def open_source(fname): + """Open a source file the best way.""" + return open(fname, "rU") + + +# Python 3.x is picky about bytes and strings, so provide methods to +# get them right, and make them no-ops in 2.x +if sys.version_info >= (3, 0): + def to_bytes(s): + """Convert string `s` to bytes.""" + return s.encode('utf8') + + def to_string(b): + """Convert bytes `b` to a string.""" + return b.decode('utf8') + +else: + def to_bytes(s): + """Convert string `s` to bytes (no-op in 2.x).""" + return s + + def to_string(b): + """Convert bytes `b` to a string (no-op in 2.x).""" + return b + +# Md5 is available in different places. +try: + import hashlib + md5 = hashlib.md5 +except ImportError: + import md5 + md5 = md5.new diff --git a/coverage/bytecode.py b/coverage/bytecode.py new file mode 100644 index 0000000..fd5c7da --- /dev/null +++ b/coverage/bytecode.py @@ -0,0 +1,91 @@ +"""Bytecode manipulation for coverage.py""" + +import opcode, sys, types + +class ByteCode(object): + """A single bytecode.""" + def __init__(self): + # The offset of this bytecode in the code object. + self.offset = -1 + + # The opcode, defined in the `opcode` module. + self.op = -1 + + # The argument, a small integer, whose meaning depends on the opcode. + self.arg = -1 + + # The offset in the code object of the next bytecode. + self.next_offset = -1 + + # The offset to jump to. + self.jump_to = -1 + + +class ByteCodes(object): + """Iterator over byte codes in `code`. + + Returns `ByteCode` objects. + + """ + # pylint: disable=R0924 + def __init__(self, code): + self.code = code + self.offset = 0 + + if sys.version_info >= (3, 0): + def __getitem__(self, i): + return self.code[i] + else: + def __getitem__(self, i): + return ord(self.code[i]) + + def __iter__(self): + return self + + def __next__(self): + if self.offset >= len(self.code): + raise StopIteration + + bc = ByteCode() + bc.op = self[self.offset] + bc.offset = self.offset + + next_offset = self.offset+1 + if bc.op >= opcode.HAVE_ARGUMENT: + bc.arg = self[self.offset+1] + 256*self[self.offset+2] + next_offset += 2 + + label = -1 + if bc.op in opcode.hasjrel: + label = next_offset + bc.arg + elif bc.op in opcode.hasjabs: + label = bc.arg + bc.jump_to = label + + bc.next_offset = self.offset = next_offset + return bc + + next = __next__ # Py2k uses an old-style non-dunder name. + + +class CodeObjects(object): + """Iterate over all the code objects in `code`.""" + def __init__(self, code): + self.stack = [code] + + def __iter__(self): + return self + + def __next__(self): + if 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) + return code + + raise StopIteration + + next = __next__ diff --git a/coverage/cmdline.py b/coverage/cmdline.py new file mode 100644 index 0000000..cb1d7a3 --- /dev/null +++ b/coverage/cmdline.py @@ -0,0 +1,730 @@ +"""Command-line support for Coverage.""" + +import optparse, sys, traceback + +from coverage.backward import sorted # pylint: disable=W0622 +from coverage.execfile import run_python_file, run_python_module +from coverage.misc import CoverageException, ExceptionDuringRun, NoSource + + +class Opts(object): + """A namespace class for individual options we'll build parsers from.""" + + append = optparse.make_option( + '-a', '--append', action='store_false', dest="erase_first", + help="Append coverage data to .coverage, otherwise it is started " + "clean with each run." + ) + branch = optparse.make_option( + '', '--branch', action='store_true', + help="Measure branch coverage in addition to statement coverage." + ) + directory = optparse.make_option( + '-d', '--directory', action='store', metavar="DIR", + help="Write the output files to DIR." + ) + fail_under = optparse.make_option( + '', '--fail-under', action='store', metavar="MIN", type="int", + help="Exit with a status of 2 if the total coverage is less than MIN." + ) + help = optparse.make_option( + '-h', '--help', action='store_true', + help="Get help on this command." + ) + ignore_errors = optparse.make_option( + '-i', '--ignore-errors', action='store_true', + help="Ignore errors while reading source files." + ) + include = optparse.make_option( + '', '--include', action='store', + metavar="PAT1,PAT2,...", + help="Include files only when their filename path matches one of " + "these patterns. Usually needs quoting on the command line." + ) + pylib = optparse.make_option( + '-L', '--pylib', action='store_true', + help="Measure coverage even inside the Python installed library, " + "which isn't done by default." + ) + show_missing = optparse.make_option( + '-m', '--show-missing', action='store_true', + help="Show line numbers of statements in each module that weren't " + "executed." + ) + old_omit = optparse.make_option( + '-o', '--omit', action='store', + metavar="PAT1,PAT2,...", + help="Omit files when their filename matches one of these patterns. " + "Usually needs quoting on the command line." + ) + omit = optparse.make_option( + '', '--omit', action='store', + metavar="PAT1,PAT2,...", + help="Omit files when their filename matches one of these patterns. " + "Usually needs quoting on the command line." + ) + output_xml = optparse.make_option( + '-o', '', action='store', dest="outfile", + metavar="OUTFILE", + help="Write the XML report to this file. Defaults to 'coverage.xml'" + ) + parallel_mode = optparse.make_option( + '-p', '--parallel-mode', action='store_true', + help="Append the machine name, process id and random number to the " + ".coverage data file name to simplify collecting data from " + "many processes." + ) + module = optparse.make_option( + '-m', '--module', action='store_true', + help=" is an importable Python module, not a script path, " + "to be run as 'python -m' would run it." + ) + rcfile = optparse.make_option( + '', '--rcfile', action='store', + help="Specify configuration file. Defaults to '.coveragerc'" + ) + source = optparse.make_option( + '', '--source', action='store', metavar="SRC1,SRC2,...", + help="A list of packages or directories of code to be measured." + ) + timid = optparse.make_option( + '', '--timid', action='store_true', + help="Use a simpler but slower trace method. Try this if you get " + "seemingly impossible results!" + ) + title = optparse.make_option( + '', '--title', action='store', metavar="TITLE", + help="A text string to use as the title on the HTML." + ) + version = optparse.make_option( + '', '--version', action='store_true', + help="Display version information and exit." + ) + + +class CoverageOptionParser(optparse.OptionParser, object): + """Base OptionParser for coverage. + + Problems don't exit the program. + Defaults are initialized for all options. + + """ + + def __init__(self, *args, **kwargs): + super(CoverageOptionParser, self).__init__( + add_help_option=False, *args, **kwargs + ) + self.set_defaults( + actions=[], + branch=None, + directory=None, + fail_under=None, + help=None, + ignore_errors=None, + include=None, + omit=None, + parallel_mode=None, + module=None, + pylib=None, + rcfile=True, + show_missing=None, + source=None, + timid=None, + title=None, + erase_first=None, + version=None, + ) + + 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.""" + pass + + def parse_args(self, args=None, options=None): + """Call optparse.parse_args, but return a triple: + + (ok, options, args) + + """ + try: + 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) + raise self.OptionParserError + + +class ClassicOptionParser(CoverageOptionParser): + """Command-line parser for coverage.py classic arguments.""" + + def __init__(self): + super(ClassicOptionParser, self).__init__() + + self.add_action('-a', '--annotate', 'annotate') + self.add_action('-b', '--html', 'html') + self.add_action('-c', '--combine', 'combine') + self.add_action('-e', '--erase', 'erase') + self.add_action('-r', '--report', 'report') + self.add_action('-x', '--execute', 'execute') + + self.add_options([ + Opts.directory, + Opts.help, + Opts.ignore_errors, + Opts.pylib, + Opts.show_missing, + Opts.old_omit, + Opts.parallel_mode, + Opts.timid, + Opts.version, + ]) + + def add_action(self, dash, dashdash, action_code): + """Add a specialized option that is the action to execute.""" + option = self.add_option(dash, dashdash, action='callback', + callback=self._append_action + ) + option.action_code = action_code + + def _append_action(self, option, opt_unused, value_unused, parser): + """Callback for an option that adds to the `actions` list.""" + parser.values.actions.append(option.action_code) + + +class CmdOptionParser(CoverageOptionParser): + """Parse one of the new-style commands for coverage.py.""" + + def __init__(self, action, options=None, defaults=None, usage=None, + cmd=None, description=None + ): + """Create an OptionParser for a coverage command. + + `action` is the slug to put into `options.actions`. + `options` is a list of Option's for the command. + `defaults` is a dict of default value for options. + `usage` is the usage string to display in help. + `cmd` is the command name, if different than `action`. + `description` is the description of the command, for the help text. + + """ + if usage: + usage = "%prog " + usage + super(CmdOptionParser, self).__init__( + prog="coverage %s" % (cmd or action), + usage=usage, + description=description, + ) + self.set_defaults(actions=[action], **(defaults or {})) + if options: + self.add_options(options) + self.cmd = cmd or action + + def __eq__(self, other): + # A convenience equality, so that I can put strings in unit test + # results, and they will compare equal to objects. + return (other == "" % self.cmd) + +GLOBAL_ARGS = [ + Opts.rcfile, + Opts.help, + ] + +CMDS = { + 'annotate': CmdOptionParser("annotate", + [ + Opts.directory, + Opts.ignore_errors, + Opts.omit, + Opts.include, + ] + GLOBAL_ARGS, + usage = "[options] [modules]", + description = "Make annotated copies of the given files, marking " + "statements that are executed with > and statements that are " + "missed with !." + ), + + 'combine': CmdOptionParser("combine", GLOBAL_ARGS, + usage = " ", + description = "Combine data from multiple coverage files collected " + "with 'run -p'. The combined results are written to a single " + "file representing the union of the data." + ), + + 'debug': CmdOptionParser("debug", GLOBAL_ARGS, + usage = "", + description = "Display information on 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." + ), + + 'erase': CmdOptionParser("erase", GLOBAL_ARGS, + usage = " ", + description = "Erase previously collected coverage data." + ), + + 'help': CmdOptionParser("help", GLOBAL_ARGS, + usage = "[command]", + description = "Describe how to use coverage.py" + ), + + 'html': CmdOptionParser("html", + [ + Opts.directory, + Opts.fail_under, + Opts.ignore_errors, + Opts.omit, + Opts.include, + Opts.title, + ] + GLOBAL_ARGS, + usage = "[options] [modules]", + description = "Create an HTML report of the coverage of the files. " + "Each file gets its own page, with the source decorated to show " + "executed, excluded, and missed lines." + ), + + 'report': CmdOptionParser("report", + [ + Opts.fail_under, + Opts.ignore_errors, + Opts.omit, + Opts.include, + Opts.show_missing, + ] + GLOBAL_ARGS, + usage = "[options] [modules]", + description = "Report coverage statistics on modules." + ), + + 'run': CmdOptionParser("execute", + [ + Opts.append, + Opts.branch, + Opts.pylib, + Opts.parallel_mode, + Opts.module, + Opts.timid, + Opts.source, + Opts.omit, + Opts.include, + ] + GLOBAL_ARGS, + defaults = {'erase_first': True}, + cmd = "run", + usage = "[options] [program options]", + description = "Run a Python program, measuring code execution." + ), + + 'xml': CmdOptionParser("xml", + [ + Opts.fail_under, + Opts.ignore_errors, + Opts.omit, + Opts.include, + Opts.output_xml, + ] + GLOBAL_ARGS, + cmd = "xml", + usage = "[options] [modules]", + description = "Generate an XML report of coverage results." + ), + } + + +OK, ERR, FAIL_UNDER = 0, 1, 2 + + +class CoverageScript(object): + """The command-line interface to Coverage.""" + + def __init__(self, _covpkg=None, _run_python_file=None, + _run_python_module=None, _help_fn=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.classic = False + + self.coverage = None + + def command_line(self, argv): + """The bulk of the command line interface to Coverage. + + `argv` is the argument list to process. + + Returns 0 if all is well, 1 if something went wrong. + + """ + # Collect the command-line options. + if not argv: + self.help_fn(topic='minimum_help') + return OK + + # The command syntax we parse depends on the first argument. Classic + # syntax always starts with an option. + self.classic = argv[0].startswith('-') + if self.classic: + parser = ClassicOptionParser() + else: + parser = CMDS.get(argv[0]) + if not parser: + self.help_fn("Unknown command: '%s'" % argv[0]) + return ERR + argv = argv[1:] + + parser.help_fn = self.help_fn + ok, options, args = parser.parse_args(argv) + if not ok: + return ERR + + # Handle help and version. + if self.do_help(options, args, parser): + return OK + + # Check for conflicts and problems in the options. + if not self.args_ok(options, args): + return ERR + + # Listify the list options. + source = unshell_list(options.source) + omit = unshell_list(options.omit) + include = unshell_list(options.include) + + # Do something. + self.coverage = self.covpkg.coverage( + data_suffix = options.parallel_mode, + cover_pylib = options.pylib, + timid = options.timid, + branch = options.branch, + config_file = options.rcfile, + source = source, + omit = omit, + include = include, + ) + + if 'debug' in options.actions: + return self.do_debug(args) + + if 'erase' in options.actions or options.erase_first: + self.coverage.erase() + else: + self.coverage.load() + + if 'execute' in options.actions: + self.do_execute(options, args) + + if 'combine' in options.actions: + self.coverage.combine() + self.coverage.save() + + # Remaining actions are reporting, with some common options. + report_args = dict( + morfs = args, + ignore_errors = options.ignore_errors, + omit = omit, + include = include, + ) + + if 'report' in options.actions: + total = self.coverage.report( + show_missing=options.show_missing, **report_args) + if 'annotate' in options.actions: + self.coverage.annotate( + directory=options.directory, **report_args) + if 'html' in options.actions: + total = self.coverage.html_report( + directory=options.directory, title=options.title, + **report_args) + if 'xml' in options.actions: + outfile = options.outfile + total = self.coverage.xml_report(outfile=outfile, **report_args) + + if options.fail_under is not None: + if total >= options.fail_under: + return OK + else: + return FAIL_UNDER + else: + 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) + print("Use 'coverage help' for help.") + elif parser: + print(parser.format_help().strip()) + else: + help_msg = HELP_TOPICS.get(topic, '').strip() + if help_msg: + print(help_msg % self.covpkg.__dict__) + else: + print("Don't know topic %r" % topic) + + def do_help(self, options, args, parser): + """Deal with help requests. + + Return True if it handled the request, False if not. + + """ + # Handle help. + if options.help: + if self.classic: + self.help_fn(topic='help') + else: + self.help_fn(parser=parser) + return True + + if "help" in options.actions: + if args: + for a in args: + parser = CMDS.get(a) + if parser: + self.help_fn(parser=parser) + else: + self.help_fn(topic=a) + else: + self.help_fn(topic='help') + return True + + # Handle version. + if options.version: + self.help_fn(topic='version') + return True + + return False + + def args_ok(self, options, args): + """Check for conflicts and problems in the options. + + Returns True if everything is ok, or False if not. + + """ + for i in ['erase', 'execute']: + for j in ['annotate', 'html', 'report', 'combine']: + if (i in options.actions) and (j in options.actions): + self.help_fn("You can't specify the '%s' and '%s' " + "options at the same time." % (i, j)) + return False + + if not options.actions: + self.help_fn( + "You must specify at least one of -e, -x, -c, -r, -a, or -b." + ) + return False + args_allowed = ( + 'execute' in options.actions or + 'annotate' in options.actions or + 'html' in options.actions or + 'debug' in options.actions or + 'report' in options.actions or + 'xml' in options.actions + ) + if not args_allowed and args: + self.help_fn("Unexpected arguments: %s" % " ".join(args)) + return False + + if 'execute' in options.actions and not args: + self.help_fn("Nothing to do.") + return False + + return True + + def do_execute(self, options, args): + """Implementation of 'coverage run'.""" + + # Run the script. + self.coverage.start() + code_ran = True + try: + try: + if options.module: + self.run_python_module(args[0], args) + else: + self.run_python_file(args[0], args) + except NoSource: + code_ran = False + raise + finally: + self.coverage.stop() + if code_ran: + self.coverage.save() + + def do_debug(self, args): + """Implementation of 'coverage debug'.""" + + if not args: + self.help_fn("What information would you like: data, sys?") + return ERR + for info in args: + if info == 'sys': + print("-- sys ----------------------------------------") + for label, info in self.coverage.sysinfo(): + if info == []: + info = "-none-" + if isinstance(info, list): + prefix = "%15s:" % label + for e in info: + print("%16s %s" % (prefix, e)) + prefix = "" + else: + print("%15s: %s" % (label, info)) + elif info == 'data': + print("-- data ---------------------------------------") + self.coverage.load() + print("path: %s" % self.coverage.data.filename) + print("has_arcs: %r" % self.coverage.data.has_arcs()) + summary = self.coverage.data.summary(fullpath=True) + if summary: + filenames = sorted(summary.keys()) + print("\n%d files:" % len(filenames)) + for f in filenames: + print("%s: %d lines" % (f, summary[f])) + else: + print("No data collected") + else: + self.help_fn("Don't know what you mean by %r" % info) + return ERR + return OK + + +def unshell_list(s): + """Turn a command-line argument into a list.""" + if not s: + return None + if sys.platform == 'win32': + # When running coverage as coverage.exe, some of the behavior + # of the shell is emulated: wildcards are expanded into a list of + # filenames. So you have to single-quote patterns on the command + # line, but (not) helpfully, the single quotes are included in the + # argument, so we have to strip them off here. + s = s.strip("'") + return s.split(',') + + +HELP_TOPICS = { +# ------------------------- +'classic': +r"""Coverage.py version %(__version__)s +Measure, collect, and report on code coverage in Python programs. + +Usage: + +coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] + Execute the module, passing the given command-line arguments, collecting + coverage data. With the -p option, include the machine name and process + id in the .coverage file name. With -L, measure coverage even inside the + Python installed library, which isn't done by default. With --timid, use a + simpler but slower trace method. + +coverage -e + Erase collected coverage data. + +coverage -c + Combine data from multiple coverage files (as created by -p option above) + and store it into a single file representing the union of the coverage. + +coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] + Report on the statement coverage for the given files. With the -m + option, show line numbers of the statements that weren't executed. + +coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] + Create an HTML report of the coverage of the given files. Each file gets + its own page, with the file listing decorated to show executed, excluded, + and missed lines. + +coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] + Make annotated copies of the given files, marking statements that + are executed with > and statements that are missed with !. + +-d DIR + Write output files for -b or -a to this directory. + +-i Ignore errors while reporting or annotating. + +-o DIR,... + Omit reporting or annotating files when their filename path starts with + a directory listed in the omit list. + e.g. coverage -i -r -o c:\python25,lib\enthought\traits + +Coverage data is saved in the file .coverage by default. Set the +COVERAGE_FILE environment variable to save it somewhere else. +""", +# ------------------------- +'help': """\ +Coverage.py, version %(__version__)s +Measure, collect, and report on code coverage in Python programs. + +usage: coverage [options] [args] + +Commands: + annotate Annotate source files with execution information. + combine Combine a number of data files. + erase Erase previously collected coverage data. + help Get help on using coverage.py. + html Create an HTML report. + report Report coverage stats on modules. + run Run a Python program and measure code execution. + xml Create an XML report of coverage results. + +Use "coverage help " for detailed help on any command. +Use "coverage help classic" for help on older command syntax. +For more information, see %(__url__)s +""", +# ------------------------- +'minimum_help': """\ +Code coverage for Python. Use 'coverage help' for help. +""", +# ------------------------- +'version': """\ +Coverage.py, version %(__version__)s. %(__url__)s +""", +} + + +def main(argv=None): + """The main entry point to Coverage. + + This is installed as the script entry point. + + """ + if argv is None: + argv = sys.argv[1:] + try: + status = CoverageScript().command_line(argv) + except ExceptionDuringRun: + # An exception was caught while running the product code. The + # sys.exc_info() return tuple is packed into an ExceptionDuringRun + # exception. + _, err, _ = sys.exc_info() + traceback.print_exception(*err.args) + status = ERR + except CoverageException: + # A controlled error inside coverage.py: print the message to the user. + _, err, _ = sys.exc_info() + print(err) + status = ERR + except SystemExit: + # The user called `sys.exit()`. Exit with their argument, if any. + _, err, _ = sys.exc_info() + if err.args: + status = err.args[0] + else: + status = None + return status diff --git a/coverage/codeunit.py b/coverage/codeunit.py new file mode 100644 index 0000000..ca1ae5c --- /dev/null +++ b/coverage/codeunit.py @@ -0,0 +1,145 @@ +"""Code unit (module) handling for Coverage.""" + +import glob, os + +from coverage.backward import open_source, string_class, StringIO +from coverage.misc import CoverageException + + +def code_unit_factory(morfs, file_locator): + """Construct a list of CodeUnits from polymorphic inputs. + + `morfs` is a module or a filename, or a list of same. + + `file_locator` is a FileLocator that can help resolve filenames. + + Returns a list of CodeUnit objects. + + """ + # Be sure we have a list. + if not isinstance(morfs, (list, tuple)): + morfs = [morfs] + + # On Windows, the shell doesn't expand wildcards. Do it here. + globbed = [] + for morf in morfs: + if isinstance(morf, string_class) and ('?' in morf or '*' in morf): + globbed.extend(glob.glob(morf)) + else: + globbed.append(morf) + morfs = globbed + + code_units = [CodeUnit(morf, file_locator) for morf in morfs] + + return code_units + + +class CodeUnit(object): + """Code unit: a filename or module. + + Instance attributes: + + `name` is a human-readable name for this code unit. + `filename` is the os path from which we can read the source. + `relative` is a boolean. + + """ + def __init__(self, morf, file_locator): + self.file_locator = file_locator + + if hasattr(morf, '__file__'): + f = morf.__file__ + else: + f = morf + # .pyc files should always refer to a .py instead. + if f.endswith('.pyc') or f.endswith('.pyo'): + f = f[:-1] + elif f.endswith('$py.class'): # Jython + f = f[:-9] + ".py" + self.filename = self.file_locator.canonical_filename(f) + + if hasattr(morf, '__name__'): + n = modname = morf.__name__ + self.relative = True + else: + n = os.path.splitext(morf)[0] + rel = self.file_locator.relative_filename(n) + if os.path.isabs(n): + self.relative = (rel != n) + else: + self.relative = True + n = rel + modname = None + self.name = n + self.modname = modname + + def __repr__(self): + return "" % (self.name, self.filename) + + # Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all + # of them defined. + + def __lt__(self, other): + return self.name < other.name + def __le__(self, other): + return self.name <= other.name + def __eq__(self, other): + return self.name == other.name + def __ne__(self, other): + return self.name != other.name + def __gt__(self, other): + return self.name > other.name + def __ge__(self, other): + return self.name >= other.name + + def flat_rootname(self): + """A base for a flat filename to correspond to this code unit. + + Useful for writing files about the code where you want all the files in + the same directory, but need to differentiate same-named files from + different directories. + + For example, the file a/b/c.py might return 'a_b_c' + + """ + if self.modname: + return self.modname.replace('.', '_') + else: + root = os.path.splitdrive(self.name)[1] + return root.replace('\\', '_').replace('/', '_').replace('.', '_') + + def source_file(self): + """Return an open file for reading the source of the code unit.""" + if os.path.exists(self.filename): + # A regular text file: open it. + return open_source(self.filename) + + # Maybe it's in a zip file? + source = self.file_locator.get_zip_data(self.filename) + if source is not None: + return StringIO(source) + + # Couldn't find source. + raise CoverageException( + "No source for code '%s'." % self.filename + ) + + def should_be_python(self): + """Does it seem like this file should contain Python? + + This is used to decide if a file reported as part of the exection of + a program was really likely to have contained Python in the first + place. + + """ + # Get the file extension. + _, ext = os.path.splitext(self.filename) + + # Anything named *.py* should be Python. + if ext.startswith('.py'): + return True + # A file with no extension should be Python. + if not ext: + return True + # Everything else is probably not Python. + return False diff --git a/coverage/collector.py b/coverage/collector.py new file mode 100644 index 0000000..96a9661 --- /dev/null +++ b/coverage/collector.py @@ -0,0 +1,340 @@ +"""Raw data collector for Coverage.""" + +import os, sys, threading + +try: + # Use the C extension code when we can, for speed. + from coverage.tracer import CTracer # pylint: disable=F0401,E0611 +except ImportError: + # Couldn't import the C extension, maybe it isn't built. + if os.getenv('COVERAGE_TEST_TRACER') == 'c': + # During testing, we use the COVERAGE_TEST_TRACER env var to indicate + # that we've fiddled with the environment to test this fallback code. + # If we thought we had a C tracer, but couldn't import it, then exit + # quickly and clearly instead of dribbling confusing errors. I'm using + # sys.exit here instead of an exception because an exception here + # causes all sorts of other noise in unittest. + sys.stderr.write( + "*** COVERAGE_TEST_TRACER is 'c' but can't import CTracer!\n" + ) + sys.exit(1) + CTracer = None + + +class PyTracer(object): + """Python implementation of the raw data tracer.""" + + # Because of poor implementations of trace-function-manipulating tools, + # the Python trace function must be kept very simple. In particular, there + # must be only one function ever set as the trace function, both through + # sys.settrace, and as the return value from the trace function. Put + # another way, the trace function must always return itself. It cannot + # swap in other functions, or return None to avoid tracing a particular + # frame. + # + # The trace manipulator that introduced this restriction is DecoratorTools, + # which sets a trace function, and then later restores the pre-existing one + # by calling sys.settrace with a function it found in the current frame. + # + # Systems that use DecoratorTools (or similar trace manipulations) must use + # PyTracer to get accurate results. The command-line --timid argument is + # used to force the use of this tracer. + + def __init__(self): + self.data = None + self.should_trace = None + self.should_trace_cache = None + self.warn = None + self.cur_file_data = None + self.last_line = 0 + self.data_stack = [] + self.last_exc_back = None + self.last_exc_firstlineno = 0 + self.arcs = False + + def _trace(self, frame, event, arg_unused): + """The trace function passed to sys.settrace.""" + + #print("trace event: %s %r @%d" % ( + # event, frame.f_code.co_filename, frame.f_lineno), + # file=sys.stderr) + + if self.last_exc_back: + if frame == self.last_exc_back: + # Someone forgot a return event. + if self.arcs and self.cur_file_data: + pair = (self.last_line, -self.last_exc_firstlineno) + self.cur_file_data[pair] = None + self.cur_file_data, self.last_line = self.data_stack.pop() + self.last_exc_back = None + + if event == 'call': + # Entering a new function context. Decide if we should trace + # in this file. + self.data_stack.append((self.cur_file_data, self.last_line)) + filename = frame.f_code.co_filename + tracename = self.should_trace_cache.get(filename) + if tracename is None: + tracename = self.should_trace(filename, frame) + self.should_trace_cache[filename] = tracename + #print("called, stack is %d deep, tracename is %r" % ( + # len(self.data_stack), tracename)) + if tracename: + if tracename not in self.data: + self.data[tracename] = {} + self.cur_file_data = self.data[tracename] + else: + self.cur_file_data = None + # Set the last_line to -1 because the next arc will be entering a + # code block, indicated by (-1, n). + self.last_line = -1 + elif event == 'line': + # Record an executed line. + if self.cur_file_data is not None: + if self.arcs: + #print("lin", self.last_line, frame.f_lineno) + self.cur_file_data[(self.last_line, frame.f_lineno)] = None + else: + #print("lin", frame.f_lineno) + self.cur_file_data[frame.f_lineno] = None + self.last_line = frame.f_lineno + elif event == 'return': + if self.arcs and self.cur_file_data: + first = frame.f_code.co_firstlineno + self.cur_file_data[(self.last_line, -first)] = None + # Leaving this function, pop the filename stack. + self.cur_file_data, self.last_line = self.data_stack.pop() + #print("returned, stack is %d deep" % (len(self.data_stack))) + elif event == 'exception': + #print("exc", self.last_line, frame.f_lineno) + self.last_exc_back = frame.f_back + self.last_exc_firstlineno = frame.f_code.co_firstlineno + return self._trace + + def start(self): + """Start this Tracer. + + Return a Python function suitable for use with sys.settrace(). + + """ + sys.settrace(self._trace) + return self._trace + + def stop(self): + """Stop this Tracer.""" + if hasattr(sys, "gettrace") and self.warn: + if sys.gettrace() != self._trace: + msg = "Trace function changed, measurement is likely wrong: %r" + self.warn(msg % (sys.gettrace(),)) + #--debug + #from coverage.misc import short_stack + #self.warn(msg % (sys.gettrace()))#, short_stack())) + sys.settrace(None) + + def get_stats(self): + """Return a dictionary of statistics, or None.""" + return None + + +class Collector(object): + """Collects trace data. + + Creates a Tracer object for each thread, since they track stack + information. Each Tracer points to the same shared data, contributing + traced data points. + + When the Collector is started, it creates a Tracer for the current thread, + and installs a function to create Tracers for each new thread started. + When the Collector is stopped, all active Tracers are stopped. + + Threads started while the Collector is stopped will never have Tracers + associated with them. + + """ + + # The stack of active Collectors. Collectors are added here when started, + # and popped when stopped. Collectors on the stack are paused when not + # the top, and resumed when they become the top again. + _collectors = [] + + def __init__(self, should_trace, timid, branch, warn): + """Create a collector. + + `should_trace` is a function, taking a filename, and returning a + canonicalized filename, or False depending on whether the file should + be traced or not. + + 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 + operate properly. + + If `branch` is true, then branches will be measured. This involves + collecting data on which statements followed each other (arcs). Use + `get_arc_data` to get the arc data. + + `warn` is a warning function, taking a single string message argument, + to be used if a warning needs to be issued. + + """ + self.should_trace = should_trace + self.warn = warn + self.branch = branch + self.reset() + + if timid: + # Being timid: use the simple Python trace function. + self._trace_class = PyTracer + else: + # Being fast: use the C Tracer if it is available, else the Python + # trace function. + self._trace_class = CTracer or PyTracer + + def __repr__(self): + return "" % id(self) + + def tracer_name(self): + """Return the class name of the tracer we're using.""" + return self._trace_class.__name__ + + def reset(self): + """Clear collected data, and prepare to collect more.""" + # A dictionary mapping filenames to dicts with linenumber keys, + # or mapping filenames to dicts with linenumber pairs as keys. + self.data = {} + + # A cache of the results from should_trace, the decision about whether + # to trace execution in a file. A dict of filename to (filename or + # False). + self.should_trace_cache = {} + + # Our active Tracers. + self.tracers = [] + + def _start_tracer(self): + """Start a new Tracer object, and store it in self.tracers.""" + tracer = self._trace_class() + tracer.data = self.data + tracer.arcs = self.branch + tracer.should_trace = self.should_trace + tracer.should_trace_cache = self.should_trace_cache + tracer.warn = self.warn + fn = tracer.start() + self.tracers.append(tracer) + return fn + + # The trace function has to be set individually on each thread before + # execution begins. Ironically, the only support the threading module has + # for running code before the thread main is the tracing function. So we + # install this as a trace function, and the first time it's called, it does + # the real trace installation. + + def _installation_trace(self, frame_unused, event_unused, arg_unused): + """Called on new threads, installs the real tracer.""" + # Remove ourselves as the trace function + sys.settrace(None) + # Install the real tracer. + fn = self._start_tracer() + # Invoke the real trace function with the current event, to be sure + # not to lose an event. + if fn: + fn = fn(frame_unused, event_unused, arg_unused) + # Return the new trace function to continue tracing in this scope. + return fn + + def start(self): + """Start collecting trace information.""" + if self._collectors: + self._collectors[-1].pause() + self._collectors.append(self) + #print("Started: %r" % self._collectors, file=sys.stderr) + + # Check to see whether we had a fullcoverage tracer installed. + traces0 = [] + if hasattr(sys, "gettrace"): + fn0 = sys.gettrace() + if fn0: + tracer0 = getattr(fn0, '__self__', None) + if tracer0: + traces0 = getattr(tracer0, 'traces', []) + + # Install the tracer on this thread. + fn = self._start_tracer() + + for args in traces0: + (frame, event, arg), lineno = args + try: + fn(frame, event, arg, lineno=lineno) + except TypeError: + raise Exception( + "fullcoverage must be run with the C trace function." + ) + + # Install our installation tracer in threading, to jump start other + # threads. + threading.settrace(self._installation_trace) + + def stop(self): + """Stop collecting trace information.""" + #print >>sys.stderr, "Stopping: %r" % self._collectors + assert self._collectors + assert self._collectors[-1] is self + + self.pause() + self.tracers = [] + + # Remove this Collector from the stack, and resume the one underneath + # (if any). + self._collectors.pop() + if self._collectors: + self._collectors[-1].resume() + + def pause(self): + """Pause tracing, but be prepared to `resume`.""" + for tracer in self.tracers: + tracer.stop() + stats = tracer.get_stats() + if stats: + print("\nCoverage.py tracer stats:") + for k in sorted(stats.keys()): + print("%16s: %s" % (k, stats[k])) + threading.settrace(None) + + def resume(self): + """Resume tracing after a `pause`.""" + for tracer in self.tracers: + tracer.start() + threading.settrace(self._installation_trace) + + def get_line_data(self): + """Return the line data collected. + + Data is { filename: { lineno: None, ...}, ...} + + """ + if self.branch: + # If we were measuring branches, then we have to re-build the dict + # to show line data. + line_data = {} + for f, arcs in self.data.items(): + line_data[f] = ldf = {} + for l1, _ in list(arcs.keys()): + if l1: + ldf[l1] = None + return line_data + else: + return self.data + + def get_arc_data(self): + """Return the arc data collected. + + Data is { filename: { (l1, l2): None, ...}, ...} + + Note that no data is collected or returned if the Collector wasn't + created with `branch` true. + + """ + if self.branch: + return self.data + else: + return {} diff --git a/coverage/config.py b/coverage/config.py new file mode 100644 index 0000000..c2ebecb --- /dev/null +++ b/coverage/config.py @@ -0,0 +1,211 @@ +"""Config file for coverage.py""" + +import os, re, sys +from coverage.backward import string_class, iitems + +# In py3, # ConfigParser was renamed to the more-standard configparser +try: + import configparser # pylint: disable=F0401 +except ImportError: + import ConfigParser as configparser + + +class HandyConfigParser(configparser.RawConfigParser): + """Our specialization of ConfigParser.""" + + def read(self, filename): + """Read a filename as UTF-8 configuration data.""" + kwargs = {} + if sys.version_info >= (3, 2): + kwargs['encoding'] = "utf-8" + return configparser.RawConfigParser.read(self, filename, **kwargs) + + def get(self, *args, **kwargs): + v = configparser.RawConfigParser.get(self, *args, **kwargs) + def dollar_replace(m): + """Called for each $replacement.""" + # Only one of the groups will have matched, just get its text. + word = [w for w in m.groups() if w is not None][0] + 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) + return v + + def getlist(self, section, option): + """Read a list of strings. + + The value of `section` and `option` is treated as a comma- and newline- + separated list of strings. Each value is stripped of whitespace. + + Returns the list of strings. + + """ + value_list = self.get(section, option) + values = [] + for value_line in value_list.split('\n'): + for value in value_line.split(','): + value = value.strip() + if value: + values.append(value) + return values + + def getlinelist(self, section, option): + """Read a list of full-line strings. + + The value of `section` and `option` is treated as a newline-separated + list of strings. Each value is stripped of whitespace. + + Returns the list of strings. + + """ + value_list = self.get(section, option) + return list(filter(None, value_list.split('\n'))) + + +# The default line exclusion regexes +DEFAULT_EXCLUDE = [ + '(?i)# *pragma[: ]*no *cover', + ] + +# The default partial branch regexes, to be modified by the user. +DEFAULT_PARTIAL = [ + '(?i)# *pragma[: ]*no *branch', + ] + +# The default partial branch regexes, based on Python semantics. +# These are any Python branching constructs that can't actually execute all +# their branches. +DEFAULT_PARTIAL_ALWAYS = [ + 'while (True|1|False|0):', + 'if (True|1|False|0):', + ] + + +class CoverageConfig(object): + """Coverage.py configuration. + + The attributes of this class are the various settings that control the + operation of coverage.py. + + """ + def __init__(self): + """Initialize the configuration attributes to their defaults.""" + # Metadata about the config. + self.attempted_config_files = [] + self.config_files = [] + + # Defaults for [run] + self.branch = False + self.cover_pylib = False + self.data_file = ".coverage" + self.parallel = False + self.timid = False + self.source = None + + # Defaults for [report] + self.exclude_list = DEFAULT_EXCLUDE[:] + self.ignore_errors = False + self.include = None + self.omit = None + self.partial_list = DEFAULT_PARTIAL[:] + self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:] + self.precision = 0 + self.show_missing = False + + # Defaults for [html] + self.html_dir = "htmlcov" + self.extra_css = None + self.html_title = "Coverage report" + + # Defaults for [xml] + self.xml_output = "coverage.xml" + + # Defaults for [paths] + self.paths = {} + + def from_environment(self, env_var): + """Read configuration from the `env_var` environment variable.""" + # Timidity: for nose users, read an environment variable. This is a + # cheap hack, since the rest of the command line arguments aren't + # recognized, but it solves some users' problems. + env = os.environ.get(env_var, '') + if env: + self.timid = ('--timid' in env) + + MUST_BE_LIST = ["omit", "include"] + + def from_args(self, **kwargs): + """Read config values from `kwargs`.""" + for k, v in iitems(kwargs): + if v is not None: + if k in self.MUST_BE_LIST and isinstance(v, string_class): + v = [v] + setattr(self, k, v) + + def from_file(self, filename): + """Read configuration from a .rc file. + + `filename` is a file name to read. + + """ + self.attempted_config_files.append(filename) + + cp = HandyConfigParser() + files_read = cp.read(filename) + if files_read is not None: # return value changed in 2.4 + self.config_files.extend(files_read) + + for option_spec in self.CONFIG_FILE_OPTIONS: + self.set_attr_from_config_option(cp, *option_spec) + + # [paths] is special + if cp.has_section('paths'): + for option in cp.options('paths'): + self.paths[option] = cp.getlist('paths', option) + + CONFIG_FILE_OPTIONS = [ + # [run] + ('branch', 'run:branch', 'boolean'), + ('cover_pylib', 'run:cover_pylib', 'boolean'), + ('data_file', 'run:data_file'), + ('include', 'run:include', 'list'), + ('omit', 'run:omit', 'list'), + ('parallel', 'run:parallel', 'boolean'), + ('source', 'run:source', 'list'), + ('timid', 'run:timid', 'boolean'), + + # [report] + ('exclude_list', 'report:exclude_lines', 'linelist'), + ('ignore_errors', 'report:ignore_errors', 'boolean'), + ('include', 'report:include', 'list'), + ('omit', 'report:omit', 'list'), + ('partial_list', 'report:partial_branches', 'linelist'), + ('partial_always_list', 'report:partial_branches_always', 'linelist'), + ('precision', 'report:precision', 'int'), + ('show_missing', 'report:show_missing', 'boolean'), + + # [html] + ('html_dir', 'html:directory'), + ('extra_css', 'html:extra_css'), + ('html_title', 'html:title'), + + # [xml] + ('xml_output', 'xml:output'), + ] + + def set_attr_from_config_option(self, cp, attr, where, type_=''): + """Set an attribute on self if it exists in the ConfigParser.""" + section, option = where.split(":") + if cp.has_option(section, option): + method = getattr(cp, 'get'+type_) + setattr(self, attr, method(section, option)) diff --git a/coverage/control.py b/coverage/control.py new file mode 100644 index 0000000..afb6137 --- /dev/null +++ b/coverage/control.py @@ -0,0 +1,720 @@ +"""Core control stuff for Coverage.""" + +import atexit, os, random, socket, sys + +from coverage.annotate import AnnotateReporter +from coverage.backward import string_class, iitems +from coverage.codeunit import code_unit_factory, CodeUnit +from coverage.collector import Collector +from coverage.config import CoverageConfig +from coverage.data import CoverageData +from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher +from coverage.files import PathAliases, find_python_files, prep_patterns +from coverage.html import HtmlReporter +from coverage.misc import CoverageException, bool_or_none, join_regex +from coverage.misc import file_be_gone +from coverage.results import Analysis, Numbers +from coverage.summary import SummaryReporter +from coverage.xmlreport import XmlReporter + +class coverage(object): + """Programmatic access to coverage.py. + + To use:: + + from coverage import coverage + + cov = coverage() + cov.start() + #.. call your code .. + cov.stop() + cov.html_report(directory='covhtml') + + """ + def __init__(self, data_file=None, data_suffix=None, cover_pylib=None, + auto_data=False, timid=None, branch=None, config_file=True, + source=None, omit=None, include=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. + + `cover_pylib` is a boolean determining whether Python code installed + with the Python interpreter is measured. This includes the Python + standard library and any packages installed with the interpreter. + + If `auto_data` is true, then any existing data file will be read when + coverage measurement starts, and data will be saved automatically when + measurement stops. + + If `timid` is true, then a slower and simpler trace function will be + used. This is important for some environments where manipulation of + tracing functions breaks the faster trace function. + + If `branch` is true, then branch coverage will be measured in addition + to the usual statement coverage. + + `config_file` determines what config file to read. If it is a string, + it is the name of the config file to read. If it is True, then a + standard file is read (".coveragerc"). If it is False, then no file is + read. + + `source` is a list of file paths or package names. Only code located + in the trees indicated by the file paths or package names will be + measured. + + `include` and `omit` are lists of filename patterns. Files that match + `include` will be measured, files that match `omit` will not. Each + will also accept a single string argument. + + """ + from coverage import __version__ + + # A record of all the warnings that have been issued. + self._warnings = [] + + # Build our configuration from a number of sources: + # 1: defaults: + self.config = CoverageConfig() + + # 2: from the coveragerc file: + if config_file: + if config_file is True: + config_file = ".coveragerc" + try: + self.config.from_file(config_file) + except ValueError: + _, err, _ = sys.exc_info() + raise CoverageException( + "Couldn't read config file %s: %s" % (config_file, err) + ) + + # 3: from environment variables: + self.config.from_environment('COVERAGE_OPTIONS') + env_data_file = os.environ.get('COVERAGE_FILE') + if env_data_file: + self.config.data_file = env_data_file + + # 4: from constructor arguments: + self.config.from_args( + data_file=data_file, cover_pylib=cover_pylib, timid=timid, + branch=branch, parallel=bool_or_none(data_suffix), + source=source, omit=omit, include=include + ) + + self.auto_data = auto_data + + # _exclude_re is a dict mapping exclusion list names to compiled + # regexes. + self._exclude_re = {} + self._exclude_regex_stale() + + self.file_locator = FileLocator() + + # The source argument can be directories or package names. + self.source = [] + self.source_pkgs = [] + for src in self.config.source or []: + if os.path.exists(src): + self.source.append(self.file_locator.canonical_filename(src)) + else: + self.source_pkgs.append(src) + + self.omit = prep_patterns(self.config.omit) + self.include = prep_patterns(self.config.include) + + self.collector = Collector( + self._should_trace, timid=self.config.timid, + branch=self.config.branch, warn=self._warn + ) + + # 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 data_suffix or self.config.parallel: + if not isinstance(data_suffix, string_class): + # if data_suffix=True, use .machinename.pid.random + data_suffix = True + else: + data_suffix = None + self.data_suffix = None + self.run_suffix = 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( + basename=self.config.data_file, + collector="coverage v%s" % __version__ + ) + + # The dirs for files considered "installed with the interpreter". + self.pylib_dirs = [] + 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, os, random, socket): + if hasattr(m, "__file__"): + m_dir = self._canonical_dir(m) + if m_dir not in self.pylib_dirs: + self.pylib_dirs.append(m_dir) + + # To avoid tracing the coverage code itself, we skip anything located + # where we are. + self.cover_dir = self._canonical_dir(__file__) + + # The matchers for _should_trace, created when tracing starts. + self.source_match = None + self.pylib_match = self.cover_match = None + self.include_match = self.omit_match = None + + # Set the reporting precision. + Numbers.set_precision(self.config.precision) + + # Is it ok for no data to be collected? + self._warn_no_data = True + self._warn_unimported_source = True + + # State machine variables: + # Have we started collecting and not stopped it? + self._started = False + # Have we measured some data and not harvested it? + self._measured = False + + atexit.register(self._atexit) + + def _canonical_dir(self, morf): + """Return the canonical directory of the module or file `morf`.""" + return os.path.split(CodeUnit(morf, self.file_locator).filename)[0] + + def _source_for_file(self, filename): + """Return the source file for `filename`.""" + if not filename.endswith(".py"): + if filename[-4:-1] == ".py": + filename = filename[:-1] + elif filename.endswith("$py.class"): # jython + filename = filename[:-9] + ".py" + return filename + + def _should_trace(self, filename, frame): + """Decide whether to trace execution in `filename` + + 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 canonicalized filename if it should be traced, False if it + should not. + + """ + if not filename: + # Empty string is pretty useless + return False + + if filename.startswith('<'): + # Lots of non-file execution is represented with artificial + # filenames like "", "", or + # "". Don't ever trace these executions, since we + # can't do anything with the data later anyway. + return False + + self._check_for_packages() + + # Compiled Python files have two filenames: frame.f_code.co_filename is + # the filename 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.get('__file__') + if dunder_file: + filename = self._source_for_file(dunder_file) + + # Jython reports the .class file to the tracer, use the source file. + if filename.endswith("$py.class"): + filename = filename[:-9] + ".py" + + canonical = self.file_locator.canonical_filename(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 not self.source_match.match(canonical): + return False + elif self.include_match: + if not self.include_match.match(canonical): + return False + 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(canonical): + return False + + # We exclude the coverage code itself, since a little of it will be + # measured otherwise. + if self.cover_match and self.cover_match.match(canonical): + return False + + # Check the file against the omit pattern. + if self.omit_match and self.omit_match.match(canonical): + return False + + return canonical + + # To log what should_trace returns, change this to "if 1:" + if 0: + _real_should_trace = _should_trace + def _should_trace(self, filename, frame): # pylint: disable=E0102 + """A logging decorator around the real _should_trace function.""" + ret = self._real_should_trace(filename, frame) + print("should_trace: %r -> %r" % (filename, ret)) + return ret + + def _warn(self, msg): + """Use `msg` as a warning.""" + self._warnings.append(msg) + sys.stderr.write("Coverage.py warning: %s\n" % msg) + + def _check_for_packages(self): + """Update the source_match matcher with latest imported packages.""" + # Our self.source_pkgs attribute is a list of package names we want to + # measure. Each time through here, we see if we've imported any of + # them yet. If so, we add its file to source_match, and we don't have + # to look for that package any more. + if self.source_pkgs: + found = [] + for pkg in self.source_pkgs: + try: + mod = sys.modules[pkg] + except KeyError: + continue + + found.append(pkg) + + try: + pkg_file = mod.__file__ + except AttributeError: + pkg_file = None + else: + d, f = os.path.split(pkg_file) + if f.startswith('__init__'): + # This is actually a package, return the directory. + pkg_file = d + else: + pkg_file = self._source_for_file(pkg_file) + pkg_file = self.file_locator.canonical_filename(pkg_file) + if not os.path.exists(pkg_file): + pkg_file = None + + if pkg_file: + self.source.append(pkg_file) + self.source_match.add(pkg_file) + else: + self._warn("Module %s has no Python source." % pkg) + + for pkg in found: + self.source_pkgs.remove(pkg) + + def use_cache(self, usecache): + """Control the use of a data file (incorrectly called a cache). + + `usecache` is true or false, whether to read and write data on disk. + + """ + self.data.usefile(usecache) + + def load(self): + """Load previously-collected coverage data from the data file.""" + self.collector.reset() + self.data.read() + + def start(self): + """Start measuring code coverage. + + Coverage measurement actually occurs in functions called after `start` + is invoked. Statements in the same scope as `start` won't be measured. + + Once you invoke `start`, you must also call `stop` eventually, or your + process might not shut down cleanly. + + """ + 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 self.auto_data: + self.load() + + # Create the matchers we need for _should_trace + if self.source or self.source_pkgs: + self.source_match = TreeMatcher(self.source) + else: + if self.cover_dir: + self.cover_match = TreeMatcher([self.cover_dir]) + if self.pylib_dirs: + self.pylib_match = TreeMatcher(self.pylib_dirs) + if self.include: + self.include_match = FnmatchMatcher(self.include) + if self.omit: + self.omit_match = FnmatchMatcher(self.omit) + + self.collector.start() + self._started = True + self._measured = True + + def stop(self): + """Stop measuring code coverage.""" + self._started = False + self.collector.stop() + + def _atexit(self): + """Clean up on process shutdown.""" + if self._started: + self.stop() + if self.auto_data: + self.save() + + def erase(self): + """Erase previously-collected coverage data. + + This removes the in-memory data collected in this session as well as + discarding the data file. + + """ + self.collector.reset() + self.data.erase() + + def clear_exclude(self, which='exclude'): + """Clear the exclude list.""" + setattr(self.config, which + "_list", []) + self._exclude_regex_stale() + + def exclude(self, regex, which='exclude'): + """Exclude source lines from execution consideration. + + A number of lists of regular expressions are maintained. Each list + selects lines that are treated differently during reporting. + + `which` determines which list is modified. The "exclude" list selects + lines that are not considered executable at all. The "partial" list + indicates lines with branches that are not taken. + + `regex` is a regular expression. The regex is added to the specified + list. If any of the regexes in the list is found in a line, the line + is marked for special treatment during reporting. + + """ + excl_list = getattr(self.config, which + "_list") + excl_list.append(regex) + self._exclude_regex_stale() + + def _exclude_regex_stale(self): + """Drop all the compiled exclusion regexes, a list was modified.""" + self._exclude_re.clear() + + def _exclude_regex(self, which): + """Return a compiled regex for the given exclusion list.""" + if which not in self._exclude_re: + excl_list = getattr(self.config, which + "_list") + self._exclude_re[which] = join_regex(excl_list) + return self._exclude_re[which] + + def get_exclude_list(self, which='exclude'): + """Return a list of excluded regex patterns. + + `which` indicates which list is desired. See `exclude` for the lists + that are available, and their meaning. + + """ + return getattr(self.config, which + "_list") + + def save(self): + """Save the collected coverage data to the data file.""" + data_suffix = self.data_suffix + if data_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: + f = open(_TEST_NAME_FILE) + test_name = f.read() + f.close() + extra = "." + test_name + data_suffix = "%s%s.%s.%06d" % ( + socket.gethostname(), extra, os.getpid(), + random.randint(0, 999999) + ) + + self._harvest_data() + self.data.write(suffix=data_suffix) + + def combine(self): + """Combine together a number of similarly-named coverage data files. + + All coverage data files whose name starts with `data_file` (from the + coverage() constructor) will be read, and combined together into the + current measurements. + + """ + aliases = None + if self.config.paths: + aliases = PathAliases(self.file_locator) + for paths in self.config.paths.values(): + result = paths[0] + for pattern in paths[1:]: + aliases.add(pattern, result) + self.data.combine_parallel_data(aliases=aliases) + + def _harvest_data(self): + """Get the collected data and reset the collector. + + Also warn about various problems collecting data. + + """ + if self._measured: + self.data.add_line_data(self.collector.get_line_data()) + self.data.add_arc_data(self.collector.get_arc_data()) + self.collector.reset() + + # If there are still entries in the source_pkgs list, then we never + # encountered those packages. + if self._warn_unimported_source: + for pkg in self.source_pkgs: + self._warn("Module %s was never imported." % pkg) + + # Find out if we got any data. + summary = self.data.summary() + if not summary and self._warn_no_data: + self._warn("No data was collected.") + + # Find files that were never executed at all. + for src in self.source: + for py_file in find_python_files(src): + py_file = self.file_locator.canonical_filename(py_file) + self.data.touch_file(py_file) + + self._measured = False + + # Backward compatibility with version 1. + def analysis(self, morf): + """Like `analysis2` but doesn't return excluded line numbers.""" + f, s, _, m, mf = self.analysis2(morf) + return f, s, m, mf + + def analysis2(self, morf): + """Analyze a module. + + `morf` is a module or a filename. It will be analyzed to determine + its coverage statistics. The return value is a 5-tuple: + + * The filename for the module. + * A list of line numbers of executable statements. + * A list of line numbers of excluded statements. + * A list of line numbers of statements not run (missing from + execution). + * A readable formatted string of the missing line numbers. + + The analysis uses the source file itself and the current measured + coverage data. + + """ + analysis = self._analyze(morf) + return ( + analysis.filename, analysis.statements, analysis.excluded, + analysis.missing, analysis.missing_formatted() + ) + + def _analyze(self, it): + """Analyze a single morf or code unit. + + Returns an `Analysis` object. + + """ + self._harvest_data() + if not isinstance(it, CodeUnit): + it = code_unit_factory(it, self.file_locator)[0] + + return Analysis(self, it) + + def report(self, morfs=None, show_missing=True, ignore_errors=None, + file=None, # pylint: disable=W0622 + omit=None, include=None + ): + """Write a summary report to `file`. + + Each module in `morfs` is listed, with counts of statements, executed + statements, missing statements, and a list of lines missed. + + `include` is a list of filename patterns. Modules whose filenames + match those patterns will be included in the report. Modules matching + `omit` will not be included in the report. + + Returns a float, the total percentage covered. + + """ + self._harvest_data() + self.config.from_args( + ignore_errors=ignore_errors, omit=omit, include=include, + show_missing=show_missing, + ) + reporter = SummaryReporter(self, self.config) + return reporter.report(morfs, outfile=file) + + def annotate(self, morfs=None, directory=None, ignore_errors=None, + omit=None, include=None): + """Annotate a list of modules. + + Each module in `morfs` is annotated. The source is written to a new + file, named with a ",cover" suffix, with each line prefixed with a + marker to indicate the coverage of the line. Covered lines have ">", + excluded lines have "-", and missing lines have "!". + + See `coverage.report()` for other arguments. + + """ + self._harvest_data() + self.config.from_args( + ignore_errors=ignore_errors, omit=omit, 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): + """Generate an HTML report. + + The HTML is written to `directory`. The file "index.html" is the + overview starting point, with links to more detailed pages for + individual modules. + + `extra_css` is a path to a file of other CSS to apply on the page. + It will be copied into the HTML directory. + + `title` is a text string (not HTML) to use as the title of the HTML + report. + + See `coverage.report()` for other arguments. + + Returns a float, the total percentage covered. + + """ + self._harvest_data() + self.config.from_args( + ignore_errors=ignore_errors, omit=omit, include=include, + html_dir=directory, extra_css=extra_css, html_title=title, + ) + reporter = HtmlReporter(self, self.config) + return reporter.report(morfs) + + def xml_report(self, morfs=None, outfile=None, ignore_errors=None, + omit=None, include=None): + """Generate an XML report of coverage results. + + The report is compatible with Cobertura reports. + + Each module in `morfs` is included in the report. `outfile` is the + path to write the file to, "-" will write to stdout. + + See `coverage.report()` for other arguments. + + Returns a float, the total percentage covered. + + """ + self._harvest_data() + self.config.from_args( + ignore_errors=ignore_errors, omit=omit, 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: + outfile = open(self.config.xml_output, "w") + file_to_close = outfile + try: + 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) + + def sysinfo(self): + """Return a list of (key, value) pairs showing internal information.""" + + import coverage as covmod + import platform, re + + try: + implementation = platform.python_implementation() + except AttributeError: + implementation = "unknown" + + info = [ + ('version', covmod.__version__), + ('coverage', covmod.__file__), + ('cover_dir', self.cover_dir), + ('pylib_dirs', self.pylib_dirs), + ('tracer', self.collector.tracer_name()), + ('config_files', self.config.attempted_config_files), + ('configs_read', self.config.config_files), + ('data_path', self.data.filename), + ('python', sys.version.replace('\n', '')), + ('platform', platform.platform()), + ('implementation', implementation), + ('executable', sys.executable), + ('cwd', os.getcwd()), + ('path', sys.path), + ('environment', [ + ("%s = %s" % (k, v)) for k, v in iitems(os.environ) + if re.search(r"^COV|^PY", k) + ]), + ] + return info + + +def process_startup(): + """Call this at Python startup to perhaps measure coverage. + + If the environment variable COVERAGE_PROCESS_START is defined, coverage + measurement is started. The value of the variable is the config file + to use. + + There are two ways to configure your Python installation to invoke this + function when Python starts: + + #. Create or append to sitecustomize.py to add these lines:: + + import coverage + coverage.process_startup() + + #. Create a .pth file in your Python installation containing:: + + import coverage; coverage.process_startup() + + """ + cps = os.environ.get("COVERAGE_PROCESS_START") + if cps: + cov = coverage(config_file=cps, auto_data=True) + cov.start() + cov._warn_no_data = False + cov._warn_unimported_source = False + + +# A hack for debugging testing in subprocesses. +_TEST_NAME_FILE = "" #"/tmp/covtest.txt" diff --git a/coverage/data.py b/coverage/data.py new file mode 100644 index 0000000..c86a77f --- /dev/null +++ b/coverage/data.py @@ -0,0 +1,270 @@ +"""Coverage data for Coverage.""" + +import os + +from coverage.backward import iitems, pickle, sorted # pylint: disable=W0622 +from coverage.files import PathAliases +from coverage.misc import file_be_gone + + +class CoverageData(object): + """Manages collected coverage data, including file storage. + + The data file format is a pickled dict, with these keys: + + * collector: a string identifying the collecting software + + * lines: a dict mapping filenames to sorted lists of line numbers + executed: + { 'file1': [17,23,45], 'file2': [1,2,3], ... } + + * arcs: a dict mapping filenames to sorted lists of line number pairs: + { 'file1': [(17,23), (17,25), (25,26)], ... } + + """ + + def __init__(self, basename=None, collector=None): + """Create a CoverageData. + + `basename` is the name of the file to use for storing data. + + `collector` is a string describing the coverage measurement software. + + """ + self.collector = collector or 'unknown' + + self.use_file = True + + # Construct the filename that will be used for data file storage, if we + # ever do any file storage. + self.filename = basename or ".coverage" + self.filename = os.path.abspath(self.filename) + + # 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: None, 47: None, ... }, + # ... + # } + # + self.lines = {} + + # 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): None, (47,48): None, ... }, + # ... + # } + # + self.arcs = {} + + def usefile(self, use_file=True): + """Set whether or not to use a disk file for data.""" + self.use_file = use_file + + def read(self): + """Read coverage data from the coverage data file (if it exists).""" + if self.use_file: + self.lines, self.arcs = self._read_file(self.filename) + else: + self.lines, self.arcs = {}, {} + + def write(self, 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. + + """ + if self.use_file: + filename = self.filename + if suffix: + filename += "." + suffix + self.write_file(filename) + + def erase(self): + """Erase the data, both in this object, and from its file storage.""" + if self.use_file: + if self.filename: + file_be_gone(self.filename) + self.lines = {} + self.arcs = {} + + def line_data(self): + """Return the map from filenames to lists of line numbers executed.""" + return dict( + [(f, sorted(lmap.keys())) for f, lmap in iitems(self.lines)] + ) + + def arc_data(self): + """Return the map from filenames to lists of line number pairs.""" + return dict( + [(f, sorted(amap.keys())) for f, amap in iitems(self.arcs)] + ) + + def write_file(self, filename): + """Write the coverage data to `filename`.""" + + # Create the file data. + data = {} + + data['lines'] = self.line_data() + arcs = self.arc_data() + if arcs: + data['arcs'] = arcs + + if self.collector: + data['collector'] = self.collector + + # Write the pickle to the file. + fdata = open(filename, 'wb') + try: + pickle.dump(data, fdata, 2) + finally: + fdata.close() + + def read_file(self, filename): + """Read the coverage data from `filename`.""" + self.lines, self.arcs = self._read_file(filename) + + def raw_data(self, filename): + """Return the raw pickled data from `filename`.""" + fdata = open(filename, 'rb') + try: + data = pickle.load(fdata) + finally: + fdata.close() + return data + + def _read_file(self, filename): + """Return the stored coverage data from the given file. + + Returns two values, suitable for assigning to `self.lines` and + `self.arcs`. + + """ + lines = {} + arcs = {} + try: + data = self.raw_data(filename) + if isinstance(data, dict): + # Unpack the 'lines' item. + lines = dict([ + (f, dict.fromkeys(linenos, None)) + for f, linenos in iitems(data.get('lines', {})) + ]) + # Unpack the 'arcs' item. + arcs = dict([ + (f, dict.fromkeys(arcpairs, None)) + for f, arcpairs in iitems(data.get('arcs', {})) + ]) + except Exception: + pass + return lines, arcs + + def combine_parallel_data(self, aliases=None): + """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. + + """ + aliases = aliases or PathAliases() + data_dir, local = os.path.split(self.filename) + localdot = local + '.' + for f in os.listdir(data_dir or '.'): + if f.startswith(localdot): + full_path = os.path.join(data_dir, f) + new_lines, new_arcs = self._read_file(full_path) + for filename, file_data in iitems(new_lines): + filename = aliases.map(filename) + self.lines.setdefault(filename, {}).update(file_data) + for filename, file_data in iitems(new_arcs): + filename = aliases.map(filename) + self.arcs.setdefault(filename, {}).update(file_data) + if f != local: + os.remove(full_path) + + def add_line_data(self, line_data): + """Add executed line data. + + `line_data` is { filename: { lineno: None, ... }, ...} + + """ + for filename, linenos in iitems(line_data): + self.lines.setdefault(filename, {}).update(linenos) + + def add_arc_data(self, arc_data): + """Add measured arc data. + + `arc_data` is { filename: { (l1,l2): None, ... }, ...} + + """ + for filename, arcs in iitems(arc_data): + self.arcs.setdefault(filename, {}).update(arcs) + + def touch_file(self, filename): + """Ensure that `filename` appears in the data, empty if needed.""" + self.lines.setdefault(filename, {}) + + def measured_files(self): + """A list of all files that had been measured.""" + return list(self.lines.keys()) + + def executed_lines(self, filename): + """A map containing all the line numbers executed in `filename`. + + If `filename` hasn't been collected at all (because it wasn't executed) + then return an empty map. + + """ + return self.lines.get(filename) or {} + + def executed_arcs(self, filename): + """A map containing all the arcs executed in `filename`.""" + return self.arcs.get(filename) or {} + + def add_to_hash(self, filename, hasher): + """Contribute `filename`'s data to the Md5Hash `hasher`.""" + hasher.update(self.executed_lines(filename)) + hasher.update(self.executed_arcs(filename)) + + def summary(self, fullpath=False): + """Return a dict summarizing the coverage data. + + Keys are based on the filenames, 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. + + """ + summ = {} + if fullpath: + filename_fn = lambda f: f + else: + filename_fn = os.path.basename + for filename, lines in iitems(self.lines): + summ[filename_fn(filename)] = len(lines) + return summ + + def has_arcs(self): + """Does this data have arcs?""" + return bool(self.arcs) + + +if __name__ == '__main__': + # Ad-hoc: show the raw data in a data file. + import pprint, sys + covdata = CoverageData() + if sys.argv[1:]: + fname = sys.argv[1] + else: + fname = covdata.filename + pprint.pprint(covdata.raw_data(fname)) diff --git a/coverage/execfile.py b/coverage/execfile.py new file mode 100644 index 0000000..587c2d3 --- /dev/null +++ b/coverage/execfile.py @@ -0,0 +1,138 @@ +"""Execute files of Python code.""" + +import imp, os, sys + +from coverage.backward import exec_code_object, open_source +from coverage.misc import NoSource, ExceptionDuringRun + + +try: + # In Py 2.x, the builtins were in __builtin__ + BUILTINS = sys.modules['__builtin__'] +except KeyError: + # In Py 3.x, they're in builtins + BUILTINS = sys.modules['builtins'] + + +def rsplit1(s, sep): + """The same as s.rsplit(sep, 1), but works in 2.3""" + parts = s.split(sep) + return sep.join(parts[:-1]), parts[-1] + + +def run_python_module(modulename, args): + """Run a python module, as though with ``python -m name args...``. + + `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. + + """ + openfile = None + glo, loc = globals(), locals() + try: + try: + # Search for the module - inside its parent package, if any - using + # standard import mechanics. + if '.' in modulename: + packagename, name = rsplit1(modulename, '.') + package = __import__(packagename, glo, loc, ['__path__']) + searchpath = package.__path__ + else: + packagename, name = None, modulename + searchpath = None # "top-level search" in imp.find_module() + openfile, pathname, _ = imp.find_module(name, searchpath) + + # Complain if this is a magic non-file module. + if openfile is None and pathname is None: + raise NoSource( + "module does not live in a file: %r" % modulename + ) + + # If `modulename` is actually a package, not a mere module, then we + # pretend to be Python 2.7 and try running its __main__.py script. + if openfile is None: + packagename = modulename + name = '__main__' + package = __import__(packagename, glo, loc, ['__path__']) + searchpath = package.__path__ + openfile, pathname, _ = imp.find_module(name, searchpath) + except ImportError: + _, err, _ = sys.exc_info() + raise NoSource(str(err)) + finally: + if openfile: + openfile.close() + + # Finally, hand the file off to run_python_file for execution. + args[0] = pathname + run_python_file(pathname, args, package=packagename) + + +def run_python_file(filename, args, package=None): + """Run a python file as if it were the main program on the command line. + + `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. + + """ + # Create a module to serve as __main__ + old_main_mod = sys.modules['__main__'] + main_mod = imp.new_module('__main__') + sys.modules['__main__'] = main_mod + main_mod.__file__ = filename + if package: + main_mod.__package__ = package + main_mod.__builtins__ = BUILTINS + + # Set sys.argv and the first path element properly. + old_argv = sys.argv + old_path0 = sys.path[0] + sys.argv = args + if package: + sys.path[0] = '' + else: + sys.path[0] = os.path.abspath(os.path.dirname(filename)) + + try: + # Open the source file. + try: + source_file = open_source(filename) + except IOError: + raise NoSource("No file to run: %r" % filename) + + try: + source = source_file.read() + finally: + source_file.close() + + # We have the source. `compile` still needs the last line to be clean, + # so make sure it is, then compile a code object from it. + if not source or source[-1] != '\n': + source += '\n' + code = compile(source, filename, "exec") + + # Execute the source file. + try: + exec_code_object(code, main_mod.__dict__) + except SystemExit: + # The user called sys.exit(). Just pass it along to the upper + # layers, where it will be handled. + raise + except: + # Something went wrong while executing the user code. + # Get the exc_info, and pack them into an exception that we can + # throw up to the outer loop. We peel two layers off the traceback + # so that the coverage.py code doesn't appear in the final printed + # traceback. + typ, err, tb = sys.exc_info() + raise ExceptionDuringRun(typ, err, tb.tb_next.tb_next) + finally: + # Restore the old __main__ + sys.modules['__main__'] = old_main_mod + + # Restore the old argv and path + sys.argv = old_argv + sys.path[0] = old_path0 diff --git a/coverage/files.py b/coverage/files.py new file mode 100644 index 0000000..3a3a773 --- /dev/null +++ b/coverage/files.py @@ -0,0 +1,289 @@ +"""File wrangling.""" + +from coverage.backward import to_string +from coverage.misc import CoverageException +import fnmatch, os, os.path, re, sys + +class FileLocator(object): + """Understand how filenames work.""" + + def __init__(self): + # The absolute path to our current directory. + self.relative_dir = os.path.normcase(abs_file(os.curdir) + os.sep) + + # Cache of results of calling the canonical_filename() method, to + # avoid duplicating work. + self.canonical_filename_cache = {} + + def relative_filename(self, filename): + """Return the relative form of `filename`. + + The filename will be relative to the current directory when the + `FileLocator` was constructed. + + """ + fnorm = os.path.normcase(filename) + if fnorm.startswith(self.relative_dir): + filename = filename[len(self.relative_dir):] + return filename + + def canonical_filename(self, filename): + """Return a canonical filename for `filename`. + + An absolute path with no redundant components and normalized case. + + """ + if filename not in self.canonical_filename_cache: + if not os.path.isabs(filename): + for path in [os.curdir] + sys.path: + if path is None: + continue + f = os.path.join(path, filename) + if os.path.exists(f): + filename = f + break + cf = abs_file(filename) + self.canonical_filename_cache[filename] = cf + return self.canonical_filename_cache[filename] + + def get_zip_data(self, filename): + """Get data from `filename` if it is a zip file path. + + Returns the string data read from the zip file, or None if no zip file + could be found or `filename` isn't in it. The data returned will be + an empty string if the file is empty. + + """ + import zipimport + markers = ['.zip'+os.sep, '.egg'+os.sep] + for marker in markers: + if marker in filename: + parts = filename.split(marker) + try: + zi = zipimport.zipimporter(parts[0]+marker[:-1]) + except zipimport.ZipImportError: + continue + try: + data = zi.get_data(parts[1]) + except IOError: + continue + return to_string(data) + return None + + +if sys.platform == 'win32': + + def actual_path(path): + """Get the actual path of `path`, including the correct case.""" + if path in actual_path.cache: + return actual_path.cache[path] + + head, tail = os.path.split(path) + if not tail: + actpath = head + elif not head: + actpath = tail + else: + head = actual_path(head) + if head in actual_path.list_cache: + files = actual_path.list_cache[head] + else: + try: + files = os.listdir(head) + except OSError: + files = [] + actual_path.list_cache[head] = files + normtail = os.path.normcase(tail) + for f in files: + if os.path.normcase(f) == normtail: + tail = f + break + actpath = os.path.join(head, tail) + actual_path.cache[path] = actpath + return actpath + + actual_path.cache = {} + actual_path.list_cache = {} + +else: + def actual_path(filename): + """The actual path for non-Windows platforms.""" + return filename + +def abs_file(filename): + """Return the absolute normalized form of `filename`.""" + path = os.path.abspath(os.path.realpath(filename)) + path = actual_path(path) + return path + + +def prep_patterns(patterns): + """Prepare the file patterns for use in a `FnmatchMatcher`. + + If a pattern starts with a wildcard, it is used as a pattern + as-is. If it does not start with a wildcard, then it is made + absolute with the current directory. + + If `patterns` is None, an empty list is returned. + + """ + patterns = patterns or [] + prepped = [] + for p in patterns or []: + if p.startswith("*") or p.startswith("?"): + prepped.append(p) + else: + prepped.append(abs_file(p)) + return prepped + + +class TreeMatcher(object): + """A matcher for files in a tree.""" + def __init__(self, directories): + self.dirs = directories[:] + + def __repr__(self): + return "" % self.dirs + + def add(self, directory): + """Add another directory to the list we match for.""" + self.dirs.append(directory) + + def match(self, fpath): + """Does `fpath` indicate a file in one of our trees?""" + for d in self.dirs: + if fpath.startswith(d): + if fpath == d: + # This is the same file! + return True + if fpath[len(d)] == os.sep: + # This is a file in the directory + return True + return False + + +class FnmatchMatcher(object): + """A matcher for files by filename pattern.""" + def __init__(self, pats): + self.pats = pats[:] + + def __repr__(self): + return "" % self.pats + + def match(self, fpath): + """Does `fpath` match one of our filename patterns?""" + for pat in self.pats: + if fnmatch.fnmatch(fpath, pat): + return True + return False + + +def sep(s): + """Find the path separator used in this string, or os.sep if none.""" + sep_match = re.search(r"[\\/]", s) + if sep_match: + the_sep = sep_match.group(0) + else: + the_sep = os.sep + return the_sep + + +class PathAliases(object): + """A collection of aliases for paths. + + When combining data files from remote machines, often the paths to source + code are different, for example, due to OS differences, or because of + serialized checkouts on continuous integration machines. + + A `PathAliases` object tracks a list of pattern/result pairs, and can + map a path through those aliases to produce a unified path. + + `locator` is a FileLocator that is used to canonicalize the results. + + """ + def __init__(self, locator=None): + self.aliases = [] + self.locator = locator + + def add(self, pattern, result): + """Add the `pattern`/`result` pair to the list of aliases. + + `pattern` is an `fnmatch`-style pattern. `result` is a simple + string. When mapping paths, if a path starts with a match against + `pattern`, then that match is replaced with `result`. This models + isomorphic source trees being rooted at different places on two + different machines. + + `pattern` can't end with a wildcard component, since that would + match an entire tree, and not just its root. + + """ + # The pattern can't end with a wildcard component. + pattern = pattern.rstrip(r"\/") + if pattern.endswith("*"): + raise CoverageException("Pattern must not end with wildcards.") + pattern_sep = sep(pattern) + pattern += pattern_sep + + # Make a regex from the pattern. fnmatch always adds a \Z or $ to + # match the whole string, which we don't want. + regex_pat = fnmatch.translate(pattern).replace(r'\Z(', '(') + if regex_pat.endswith("$"): + regex_pat = regex_pat[:-1] + # We want */a/b.py to match on Windows to, 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) + + # Normalize the result: it must end with a path separator. + result_sep = sep(result) + result = result.rstrip(r"\/") + result_sep + self.aliases.append((regex, result, pattern_sep, result_sep)) + + def map(self, path): + """Map `path` through the aliases. + + `path` is checked against all of the patterns. The first pattern to + match is used to replace the root of the path with the result root. + Only one pattern is ever used. If no patterns match, `path` is + returned unchanged. + + The separator style in the result is made to match that of the result + in the alias. + + """ + for regex, result, pattern_sep, result_sep in self.aliases: + m = regex.match(path) + if m: + new = path.replace(m.group(0), result) + if pattern_sep != result_sep: + new = new.replace(pattern_sep, result_sep) + if self.locator: + new = self.locator.canonical_filename(new) + return new + return path + + +def find_python_files(dirname): + """Yield all of the importable Python files in `dirname`, recursively. + + To be importable, the files have to be in a directory with a __init__.py, + except for `dirname` itself, which isn't required to have one. The + assumption is that `dirname` was specified directly, so the user knows + best, but subdirectories are checked for a __init__.py to be sure we only + find the importable files. + + """ + for i, (dirpath, dirnames, filenames) in enumerate(os.walk(dirname)): + if i > 0 and '__init__.py' not in filenames: + # If a directory doesn't have __init__.py, then it isn't + # importable and neither are its files + del dirnames[:] + continue + for filename in filenames: + # We're only interested in files that look like reasonable Python + # files: Must end with .py, and must not have certain funny + # characters that probably mean they are editor junk. + if re.match(r"^[^.#~!$@%^&*()+=,]+\.py$", filename): + yield os.path.join(dirpath, filename) diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py new file mode 100644 index 0000000..6a258d6 --- /dev/null +++ b/coverage/fullcoverage/encodings.py @@ -0,0 +1,57 @@ +"""Imposter encodings module that installs a coverage-style tracer. + +This is NOT the encodings module; it is an imposter that sets up tracing +instrumentation and then replaces itself with the real encodings module. + +If the directory that holds this file is placed first in the PYTHONPATH when +using "coverage" to run Python's tests, then this file will become the very +first module imported by the internals of Python 3. It installs a +coverage-compatible trace function that can watch Standard Library modules +execute from the very earliest stages of Python's own boot process. This fixes +a problem with coverage - that it starts too late to trace the coverage of many +of the most fundamental modules in the Standard Library. + +""" + +import sys + +class FullCoverageTracer(object): + def __init__(self): + # `traces` is a list of trace events. Frames are tricky: the same + # frame object is used for a whole scope, with new line numbers + # written into it. So in one scope, all the frame objects are the + # same object, and will eventually all will point to the last line + # executed. So we keep the line numbers alongside the frames. + # The list looks like: + # + # traces = [ + # ((frame, event, arg), lineno), ... + # ] + # + self.traces = [] + + def fullcoverage_trace(self, *args): + frame, event, arg = args + self.traces.append((args, frame.f_lineno)) + return self.fullcoverage_trace + +sys.settrace(FullCoverageTracer().fullcoverage_trace) + +# In coverage/files.py is actual_filename(), which uses glob.glob. I don't +# understand why, but that use of glob borks everything if fullcoverage is in +# effect. So here we make an ugly hail-mary pass to switch off glob.glob over +# there. This means when using fullcoverage, Windows path names will not be +# their actual case. + +#sys.fullcoverage = True + +# Finally, remove our own directory from sys.path; remove ourselves from +# sys.modules; and re-import "encodings", which will be the real package +# this time. Note that the delete from sys.modules dictionary has to +# happen last, since all of the symbols in this module will become None +# at that exact moment, including "sys". + +parentdir = max(filter(__file__.startswith, sys.path), key=len) +sys.path.remove(parentdir) +del sys.modules['encodings'] +import encodings diff --git a/coverage/html.py b/coverage/html.py new file mode 100644 index 0000000..ed8920f --- /dev/null +++ b/coverage/html.py @@ -0,0 +1,367 @@ +"""HTML reporting for Coverage.""" + +import os, re, shutil, sys + +import coverage +from coverage.backward import pickle +from coverage.misc import CoverageException, Hasher +from coverage.phystokens import source_token_lines, source_encoding +from coverage.report import Reporter +from coverage.results import Numbers +from coverage.templite import Templite + +# Disable pylint msg W0612, because a bunch of variables look unused, but +# they're accessed in a Templite context via locals(). +# pylint: disable=W0612 + +def data_filename(fname): + """Return the path to a data file of ours.""" + return os.path.join(os.path.split(__file__)[0], fname) + +def data(fname): + """Return the contents of a data file of ours.""" + data_file = open(data_filename(fname)) + try: + return data_file.read() + finally: + data_file.close() + + +class HtmlReporter(Reporter): + """HTML reporting.""" + + # These files will be copied from the htmlfiles dir to the output dir. + STATIC_FILES = [ + "style.css", + "jquery-1.4.3.min.js", + "jquery.hotkeys.js", + "jquery.isonscreen.js", + "jquery.tablesorter.min.js", + "coverage_html.js", + "keybd_closed.png", + "keybd_open.png", + ] + + def __init__(self, cov, config): + super(HtmlReporter, self).__init__(cov, config) + self.directory = None + self.template_globals = { + 'escape': escape, + 'title': self.config.html_title, + '__url__': coverage.__url__, + '__version__': coverage.__version__, + } + self.source_tmpl = Templite( + data("htmlfiles/pyfile.html"), self.template_globals + ) + + self.coverage = cov + + self.files = [] + self.arcs = self.coverage.data.has_arcs() + self.status = HtmlStatus() + self.extra_css = None + self.totals = Numbers() + + def report(self, morfs): + """Generate an HTML report for `morfs`. + + `morfs` is a list of modules or filenames. + + """ + 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.digest() + 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) + + # Process all the files. + self.report_files(self.html_file, morfs, self.config.html_dir) + + if not self.files: + raise CoverageException("No data to report.") + + # Write the index file. + self.index_file() + + self.make_local_static_report_files() + + return self.totals.pc_covered + + def make_local_static_report_files(self): + """Make local instances of static files for HTML report.""" + # The files we provide must always be copied. + for static in self.STATIC_FILES: + shutil.copyfile( + data_filename("htmlfiles/" + static), + os.path.join(self.directory, static) + ) + + # The user may have extra CSS they want copied. + if self.extra_css: + shutil.copyfile( + self.config.extra_css, + os.path.join(self.directory, self.extra_css) + ) + + def write_html(self, fname, html): + """Write `html` to `fname`, properly encoded.""" + fout = open(fname, "wb") + try: + fout.write(html.encode('ascii', 'xmlcharrefreplace')) + finally: + fout.close() + + def file_hash(self, source, cu): + """Compute a hash that changes if the file needs to be re-reported.""" + m = Hasher() + m.update(source) + self.coverage.data.add_to_hash(cu.filename, m) + return m.digest() + + def html_file(self, cu, analysis): + """Generate an HTML file for one source file.""" + source_file = cu.source_file() + try: + source = source_file.read() + finally: + source_file.close() + + # Find out if the file on disk is already correct. + flat_rootname = cu.flat_rootname() + this_hash = self.file_hash(source, cu) + that_hash = self.status.file_hash(flat_rootname) + if this_hash == that_hash: + # Nothing has changed to require the file to be reported again. + self.files.append(self.status.index_info(flat_rootname)) + return + + self.status.set_file_hash(flat_rootname, this_hash) + + # If need be, determine the encoding of the source file. We use it + # later to properly write the HTML. + if sys.version_info < (3, 0): + encoding = source_encoding(source) + # Some UTF8 files have the dreaded UTF8 BOM. If so, junk it. + if encoding.startswith("utf-8") and source[:3] == "\xef\xbb\xbf": + source = source[3:] + encoding = "utf-8" + + # Get the numbers for this file. + nums = analysis.numbers + + missing_branch_arcs = analysis.missing_branch_arcs() + arcs = self.arcs + + # 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(source_token_lines(source)): + lineno += 1 # 1-based line numbers. + # Figure out how to mark this line. + line_class = [] + annotate_html = "" + annotate_title = "" + 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.arcs and lineno in missing_branch_arcs: + line_class.append(c_par) + annlines = [] + for b in missing_branch_arcs[lineno]: + if b < 0: + annlines.append("exit") + else: + annlines.append(str(b)) + annotate_html = "   ".join(annlines) + if len(annlines) > 1: + annotate_title = "no jumps to these line numbers" + elif len(annlines) == 1: + annotate_title = "no jump to this line number" + elif lineno in analysis.statements: + line_class.append(c_run) + + # Build the HTML for the line + html = [] + for tok_type, tok_text in line: + if tok_type == "ws": + html.append(escape(tok_text)) + else: + tok_html = escape(tok_text) or ' ' + html.append( + "%s" % (tok_type, tok_html) + ) + + lines.append({ + 'html': ''.join(html), + 'number': lineno, + 'class': ' '.join(line_class) or "pln", + 'annotate': annotate_html, + 'annotate_title': annotate_title, + }) + + # Write the HTML page for this file. + html_filename = flat_rootname + ".html" + html_path = os.path.join(self.directory, html_filename) + extra_css = self.extra_css + + html = spaceless(self.source_tmpl.render(locals())) + if sys.version_info < (3, 0): + html = html.decode(encoding) + self.write_html(html_path, html) + + # Save this file's information for the index file. + index_info = { + 'nums': nums, + 'html_filename': html_filename, + 'name': cu.name, + } + self.files.append(index_info) + self.status.set_index_info(flat_rootname, index_info) + + def index_file(self): + """Write the index.html file for this report.""" + index_tmpl = Templite( + data("htmlfiles/index.html"), self.template_globals + ) + + files = self.files + arcs = self.arcs + + self.totals = totals = sum([f['nums'] for f in files]) + extra_css = self.extra_css + + html = index_tmpl.render(locals()) + if sys.version_info < (3, 0): + html = html.decode("utf-8") + self.write_html( + os.path.join(self.directory, "index.html"), + html + ) + + # Write the latest hashes for next time. + self.status.write(self.directory) + + +class HtmlStatus(object): + """The status information we keep to support incremental reporting.""" + + STATUS_FILE = "status.dat" + STATUS_FORMAT = 1 + + def __init__(self): + self.reset() + + def reset(self): + """Initialize to empty.""" + self.settings = '' + self.files = {} + + def read(self, directory): + """Read the last status in `directory`.""" + usable = False + try: + status_file = os.path.join(directory, self.STATUS_FILE) + fstatus = open(status_file, "rb") + try: + status = pickle.load(fstatus) + finally: + fstatus.close() + except (IOError, ValueError): + usable = False + else: + usable = True + if status['format'] != self.STATUS_FORMAT: + usable = False + elif status['version'] != coverage.__version__: + usable = False + + if usable: + self.files = status['files'] + self.settings = status['settings'] + else: + self.reset() + + def write(self, directory): + """Write the current status to `directory`.""" + status_file = os.path.join(directory, self.STATUS_FILE) + status = { + 'format': self.STATUS_FORMAT, + 'version': coverage.__version__, + 'settings': self.settings, + 'files': self.files, + } + fout = open(status_file, "wb") + try: + pickle.dump(status, fout) + finally: + fout.close() + + def settings_hash(self): + """Get the hash of the coverage.py settings.""" + return self.settings + + def set_settings_hash(self, settings): + """Set the hash of the coverage.py settings.""" + self.settings = settings + + def file_hash(self, fname): + """Get the hash of `fname`'s contents.""" + return self.files.get(fname, {}).get('hash', '') + + def set_file_hash(self, fname, val): + """Set the hash of `fname`'s contents.""" + self.files.setdefault(fname, {})['hash'] = val + + def index_info(self, fname): + """Get the information for index.html for `fname`.""" + return self.files.get(fname, {}).get('index', {}) + + def set_index_info(self, fname, info): + """Set the information for index.html for `fname`.""" + self.files.setdefault(fname, {})['index'] = info + + +# Helpers for templates and generating HTML + +def escape(t): + """HTML-escape the text in `t`.""" + return (t + # Convert HTML special chars into HTML entities. + .replace("&", "&").replace("<", "<").replace(">", ">") + .replace("'", "'").replace('"', """) + # Convert runs of spaces: "......" -> " . . ." + .replace(" ", "  ") + # To deal with odd-length runs, convert the final pair of spaces + # so that "....." -> " .  ." + .replace(" ", "  ") + ) + +def spaceless(html): + """Squeeze out some annoying extra space from an HTML string. + + Nicely-formatted templates mean lots of extra space in the result. + Get rid of some. + + """ + html = re.sub(r">\s+

\n

-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; + } + } + } + + // 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(); + + // Watch for page unload events so we can save the final sort settings: + $(window).unload(function () { + document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"; + }); +}; + +// -- 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] === 'n') { + $(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.toggle_lines = function (btn, cls) { + btn = $(btn); + var hide = "hide_"+cls; + if (btn.hasClass(hide)) { + $("#source ."+cls).removeClass(hide); + btn.removeClass(hide); + } + else { + $("#source ."+cls).addClass(hide); + btn.addClass(hide); + } +}; + +// 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); +}; + +// 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. + 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(); +}; + +coverage.is_transparent = function (color) { + // Different browsers return different colors for "none". + return color === "transparent" || color === "rgba(0, 0, 0, 0)"; +}; + +coverage.to_next_chunk = function () { + var c = coverage; + + // Find the start of the next colored chunk. + var probe = c.sel_end; + while (true) { + var probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + var color = probe_line.css("background-color"); + if (!c.is_transparent(color)) { + break; + } + probe++; + } + + // There's a next chunk, `probe` points to it. + var begin = probe; + + // Find the end of this chunk. + var next_color = color; + while (next_color === color) { + probe++; + probe_line = c.line_elt(probe); + next_color = probe_line.css("background-color"); + } + 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 color = probe_line.css("background-color"); + while (probe > 0 && c.is_transparent(color)) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + return; + } + color = probe_line.css("background-color"); + } + + // 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) { + probe--; + probe_line = c.line_elt(probe); + prev_color = probe_line.css("background-color"); + } + 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_color = probe_line.css("background-color"); + if (!c.is_transparent(the_color)) { + // 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) { + probe--; + probe_line = c.line_elt(probe); + if (probe_line.length === 0) { + break; + } + color = probe_line.css("background-color"); + } + var begin = probe + 1; + + // Search forward for the last line. + probe = lineno; + color = the_color; + while (color === the_color) { + probe++; + probe_line = c.line_elt(probe); + color = probe_line.css("background-color"); + } + + coverage.set_sel(begin, probe); + } + else { + coverage.set_sel(lineno); + } +}; + +coverage.show_selection = function () { + var c = coverage; + + // Highlight the lines in the chunk + c.code_container().find(".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); +}; diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html new file mode 100644 index 0000000..c649a83 --- /dev/null +++ b/coverage/htmlfiles/index.html @@ -0,0 +1,104 @@ + + + + + {{ title|escape }} + + {% if extra_css %} + + {% endif %} + + + + + + + + +

+ +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + {% if arcs %} + b + p + {% endif %} + c   change column sorting +

+
+
+ +
+ + + {# The title='' attr doesn't work in Safari. #} + + + + + + {% if arcs %} + + + {% endif %} + + + + {# HTML syntax requires thead, tfoot, tbody #} + + + + + + + {% if arcs %} + + + {% endif %} + + + + + {% for file in files %} + + + + + + {% if arcs %} + + + {% endif %} + + + {% endfor %} + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total{{totals.n_statements}}{{totals.n_missing}}{{totals.n_excluded}}{{totals.n_branches}}{{totals.n_partial_branches}}{{totals.pc_covered_str}}%
{{file.name}}{{file.nums.n_statements}}{{file.nums.n_missing}}{{file.nums.n_excluded}}{{file.nums.n_branches}}{{file.nums.n_partial_branches}}{{file.nums.pc_covered_str}}%
+
+ + + + + diff --git a/coverage/htmlfiles/jquery-1.4.3.min.js b/coverage/htmlfiles/jquery-1.4.3.min.js new file mode 100644 index 0000000..c941a5f --- /dev/null +++ b/coverage/htmlfiles/jquery-1.4.3.min.js @@ -0,0 +1,166 @@ +/*! + * jQuery JavaScript Library v1.4.3 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Oct 14 23:10:06 2010 -0400 + */ +(function(E,A){function U(){return false}function ba(){return true}function ja(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ga(a){var b,d,e=[],f=[],h,k,l,n,s,v,B,D;k=c.data(this,this.nodeType?"events":"__events__");if(typeof k==="function")k=k.events;if(!(a.liveFired===this||!k||!k.live||a.button&&a.type==="click")){if(a.namespace)D=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var H=k.live.slice(0);for(n=0;nd)break;a.currentTarget=f.elem;a.data=f.handleObj.data; +a.handleObj=f.handleObj;D=f.handleObj.origHandler.apply(f.elem,arguments);if(D===false||a.isPropagationStopped()){d=f.level;if(D===false)b=false}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(Ha,"`").replace(Ia,"&")}function ka(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Ja.test(b))return c.filter(b, +e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function la(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this,e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var k in e[h])c.event.add(this,h,e[h][k],e[h][k].data)}}})}function Ka(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)} +function ma(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?La:Ma,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a,"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function ca(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Na.test(a)?e(a,h):ca(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)? +e(a,""):c.each(b,function(f,h){ca(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(na.concat.apply([],na.slice(0,b)),function(){d[this]=a});return d}function oa(a){if(!da[a]){var b=c("<"+a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";da[a]=d}return da[a]}function ea(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var u=E.document,c=function(){function a(){if(!b.isReady){try{u.documentElement.doScroll("left")}catch(i){setTimeout(a, +1);return}b.ready()}}var b=function(i,r){return new b.fn.init(i,r)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,k=/\S/,l=/^\s+/,n=/\s+$/,s=/\W/,v=/\d/,B=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,D=/^[\],:{}\s]*$/,H=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,G=/(?:^|:|,)(?:\s*\[)+/g,M=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,j=/(msie) ([\w.]+)/,o=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false, +q=[],t,x=Object.prototype.toString,C=Object.prototype.hasOwnProperty,P=Array.prototype.push,N=Array.prototype.slice,R=String.prototype.trim,Q=Array.prototype.indexOf,L={};b.fn=b.prototype={init:function(i,r){var y,z,F;if(!i)return this;if(i.nodeType){this.context=this[0]=i;this.length=1;return this}if(i==="body"&&!r&&u.body){this.context=u;this[0]=u.body;this.selector="body";this.length=1;return this}if(typeof i==="string")if((y=h.exec(i))&&(y[1]||!r))if(y[1]){F=r?r.ownerDocument||r:u;if(z=B.exec(i))if(b.isPlainObject(r)){i= +[u.createElement(z[1])];b.fn.attr.call(i,r,true)}else i=[F.createElement(z[1])];else{z=b.buildFragment([y[1]],[F]);i=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this,i)}else{if((z=u.getElementById(y[2]))&&z.parentNode){if(z.id!==y[2])return f.find(i);this.length=1;this[0]=z}this.context=u;this.selector=i;return this}else if(!r&&!s.test(i)){this.selector=i;this.context=u;i=u.getElementsByTagName(i);return b.merge(this,i)}else return!r||r.jquery?(r||f).find(i):b(r).find(i); +else if(b.isFunction(i))return f.ready(i);if(i.selector!==A){this.selector=i.selector;this.context=i.context}return b.makeArray(i,this)},selector:"",jquery:"1.4.3",length:0,size:function(){return this.length},toArray:function(){return N.call(this,0)},get:function(i){return i==null?this.toArray():i<0?this.slice(i)[0]:this[i]},pushStack:function(i,r,y){var z=b();b.isArray(i)?P.apply(z,i):b.merge(z,i);z.prevObject=this;z.context=this.context;if(r==="find")z.selector=this.selector+(this.selector?" ": +"")+y;else if(r)z.selector=this.selector+"."+r+"("+y+")";return z},each:function(i,r){return b.each(this,i,r)},ready:function(i){b.bindReady();if(b.isReady)i.call(u,b);else q&&q.push(i);return this},eq:function(i){return i===-1?this.slice(i):this.slice(i,+i+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(i){return this.pushStack(b.map(this,function(r,y){return i.call(r, +y,r)}))},end:function(){return this.prevObject||b(null)},push:P,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var i=arguments[0]||{},r=1,y=arguments.length,z=false,F,I,K,J,fa;if(typeof i==="boolean"){z=i;i=arguments[1]||{};r=2}if(typeof i!=="object"&&!b.isFunction(i))i={};if(y===r){i=this;--r}for(;r0)){if(q){for(var r=0;i=q[r++];)i.call(u,b);q=null}b.fn.triggerHandler&&b(u).triggerHandler("ready")}}},bindReady:function(){if(!p){p=true;if(u.readyState==="complete")return setTimeout(b.ready, +1);if(u.addEventListener){u.addEventListener("DOMContentLoaded",t,false);E.addEventListener("load",b.ready,false)}else if(u.attachEvent){u.attachEvent("onreadystatechange",t);E.attachEvent("onload",b.ready);var i=false;try{i=E.frameElement==null}catch(r){}u.documentElement.doScroll&&i&&a()}}},isFunction:function(i){return b.type(i)==="function"},isArray:Array.isArray||function(i){return b.type(i)==="array"},isWindow:function(i){return i&&typeof i==="object"&&"setInterval"in i},isNaN:function(i){return i== +null||!v.test(i)||isNaN(i)},type:function(i){return i==null?String(i):L[x.call(i)]||"object"},isPlainObject:function(i){if(!i||b.type(i)!=="object"||i.nodeType||b.isWindow(i))return false;if(i.constructor&&!C.call(i,"constructor")&&!C.call(i.constructor.prototype,"isPrototypeOf"))return false;for(var r in i);return r===A||C.call(i,r)},isEmptyObject:function(i){for(var r in i)return false;return true},error:function(i){throw i;},parseJSON:function(i){if(typeof i!=="string"||!i)return null;i=b.trim(i); +if(D.test(i.replace(H,"@").replace(w,"]").replace(G,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(i):(new Function("return "+i))();else b.error("Invalid JSON: "+i)},noop:function(){},globalEval:function(i){if(i&&k.test(i)){var r=u.getElementsByTagName("head")[0]||u.documentElement,y=u.createElement("script");y.type="text/javascript";if(b.support.scriptEval)y.appendChild(u.createTextNode(i));else y.text=i;r.insertBefore(y,r.firstChild);r.removeChild(y)}},nodeName:function(i,r){return i.nodeName&&i.nodeName.toUpperCase()=== +r.toUpperCase()},each:function(i,r,y){var z,F=0,I=i.length,K=I===A||b.isFunction(i);if(y)if(K)for(z in i){if(r.apply(i[z],y)===false)break}else for(;F";a=u.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var s=u.createElement("div"); +s.style.width=s.style.paddingLeft="1px";u.body.appendChild(s);c.boxModel=c.support.boxModel=s.offsetWidth===2;if("zoom"in s.style){s.style.display="inline";s.style.zoom=1;c.support.inlineBlockNeedsLayout=s.offsetWidth===2;s.style.display="";s.innerHTML="
";c.support.shrinkWrapBlocks=s.offsetWidth!==2}s.innerHTML="
t
";var v=s.getElementsByTagName("td");c.support.reliableHiddenOffsets=v[0].offsetHeight=== +0;v[0].style.display="";v[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&v[0].offsetHeight===0;s.innerHTML="";u.body.removeChild(s).style.display="none"});a=function(s){var v=u.createElement("div");s="on"+s;var B=s in v;if(!B){v.setAttribute(s,"return;");B=typeof v[s]==="function"}return B};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength", +cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var pa={},Oa=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?pa:a;var e=a.nodeType,f=e?a[c.expando]:null,h=c.cache;if(!(e&&!f&&typeof b==="string"&&d===A)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]= +c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==A)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?pa:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando);else if(d)delete f[e];else for(var k in a)delete a[k]}},acceptData:function(a){if(a.nodeName){var b= +c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){if(typeof a==="undefined")return this.length?c.data(this[0]):null;else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===A){var e=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(e===A&&this.length){e=c.data(this[0],a);if(e===A&&this[0].nodeType===1){e=this[0].getAttribute("data-"+a);if(typeof e=== +"string")try{e=e==="true"?true:e==="false"?false:e==="null"?null:!c.isNaN(e)?parseFloat(e):Oa.test(e)?c.parseJSON(e):e}catch(f){}else e=A}}return e===A&&d[1]?this.data(d[0]):e}else return this.each(function(){var h=c(this),k=[d[0],b];h.triggerHandler("setData"+d[1]+"!",k);c.data(this,a,b);h.triggerHandler("changeData"+d[1]+"!",k)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var e=c.data(a,b);if(!d)return e|| +[];if(!e||c.isArray(d))e=c.data(a,b,c.makeArray(d));else e.push(d);return e}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),e=d.shift();if(e==="inprogress")e=d.shift();if(e){b==="fx"&&d.unshift("inprogress");e.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===A)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this, +a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var qa=/[\n\t]/g,ga=/\s+/,Pa=/\r/g,Qa=/^(?:href|src|style)$/,Ra=/^(?:button|input)$/i,Sa=/^(?:button|input|object|select|textarea)$/i,Ta=/^a(?:rea)?$/i,ra=/^(?:radio|checkbox)$/i;c.fn.extend({attr:function(a,b){return c.access(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this, +a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(s){var v=c(this);v.addClass(a.call(this,s,v.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ga),d=0,e=this.length;d-1)return true;return false}, +val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one";if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var B=c.makeArray(v);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),B)>=0});if(!B.length)this.selectedIndex=-1}else this.value=v}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return A;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==A;b=e&&c.props[b]||b;if(a.nodeType===1){var h=Qa.test(b);if((b in a||a[b]!==A)&&e&&!h){if(f){b==="type"&&Ra.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Sa.test(a.nodeName)||Ta.test(a.nodeName)&&a.href?0:A;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return A;a=!c.support.hrefNormalized&&e&& +h?a.getAttribute(b,2):a.getAttribute(b);return a===null?A:a}}});var X=/\.(.*)$/,ha=/^(?:textarea|input|select)$/i,Ha=/\./g,Ia=/ /g,Ua=/[^\w\s.|`]/g,Va=function(a){return a.replace(Ua,"\\$&")},sa={focusin:0,focusout:0};c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var k=a.nodeType?"events":"__events__",l=h[k],n=h.handle;if(typeof l=== +"function"){n=l.handle;l=l.events}else if(!l){a.nodeType||(h[k]=h=function(){});h.events=l={}}if(!n)h.handle=n=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(n.elem,arguments):A};n.elem=a;b=b.split(" ");for(var s=0,v;k=b[s++];){h=f?c.extend({},f):{handler:d,data:e};if(k.indexOf(".")>-1){v=k.split(".");k=v.shift();h.namespace=v.slice(0).sort().join(".")}else{v=[];h.namespace=""}h.type=k;if(!h.guid)h.guid=d.guid;var B=l[k],D=c.event.special[k]||{};if(!B){B=l[k]=[]; +if(!D.setup||D.setup.call(a,e,v,n)===false)if(a.addEventListener)a.addEventListener(k,n,false);else a.attachEvent&&a.attachEvent("on"+k,n)}if(D.add){D.add.call(a,h);if(!h.handler.guid)h.handler.guid=d.guid}B.push(h);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,k=0,l,n,s,v,B,D,H=a.nodeType?"events":"__events__",w=c.data(a),G=w&&w[H];if(w&&G){if(typeof G==="function"){w=G;G=G.events}if(b&&b.type){d=b.handler;b=b.type}if(!b|| +typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in G)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[k++];){v=f;l=f.indexOf(".")<0;n=[];if(!l){n=f.split(".");f=n.shift();s=RegExp("(^|\\.)"+c.map(n.slice(0).sort(),Va).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(B=G[f])if(d){v=c.event.special[f]||{};for(h=e||0;h=0){a.type= +f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return A;a.result=A;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)=== +false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){e=a.target;var k,l=f.replace(X,""),n=c.nodeName(e,"a")&&l==="click",s=c.event.special[l]||{};if((!s._default||s._default.call(d,a)===false)&&!n&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[l]){if(k=e["on"+l])e["on"+l]=null;c.event.triggered=true;e[l]()}}catch(v){}if(k)e["on"+l]=k;c.event.triggered=false}}},handle:function(a){var b,d,e; +d=[];var f,h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var k=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ha.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=va(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===A||f===e))if(e!=null||f){a.type="change";a.liveFired= +A;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",va(a))}},setup:function(){if(this.type=== +"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ha.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ha.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}u.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){sa[b]++===0&&u.addEventListener(a,d,true)},teardown:function(){--sa[b]=== +0&&u.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=A}var k=b==="one"?c.proxy(f,function(n){c(this).unbind(n,k);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var l=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); +(function(){function a(g,j,o,m,p,q){p=0;for(var t=m.length;p0){C=x;break}}x=x[g]}m[p]=C}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,k=true;[0,0].sort(function(){k=false;return 0});var l=function(g,j,o,m){o=o||[];var p=j=j||u;if(j.nodeType!==1&&j.nodeType!==9)return[];if(!g||typeof g!=="string")return o;var q=[],t,x,C,P,N=true,R=l.isXML(j),Q=g,L;do{d.exec("");if(t=d.exec(Q)){Q=t[3];q.push(t[1]);if(t[2]){P=t[3]; +break}}}while(t);if(q.length>1&&s.exec(g))if(q.length===2&&n.relative[q[0]])x=M(q[0]+q[1],j);else for(x=n.relative[q[0]]?[j]:l(q.shift(),j);q.length;){g=q.shift();if(n.relative[g])g+=q.shift();x=M(g,x)}else{if(!m&&q.length>1&&j.nodeType===9&&!R&&n.match.ID.test(q[0])&&!n.match.ID.test(q[q.length-1])){t=l.find(q.shift(),j,R);j=t.expr?l.filter(t.expr,t.set)[0]:t.set[0]}if(j){t=m?{expr:q.pop(),set:D(m)}:l.find(q.pop(),q.length===1&&(q[0]==="~"||q[0]==="+")&&j.parentNode?j.parentNode:j,R);x=t.expr?l.filter(t.expr, +t.set):t.set;if(q.length>0)C=D(x);else N=false;for(;q.length;){t=L=q.pop();if(n.relative[L])t=q.pop();else L="";if(t==null)t=j;n.relative[L](C,t,R)}}else C=[]}C||(C=x);C||l.error(L||g);if(f.call(C)==="[object Array]")if(N)if(j&&j.nodeType===1)for(g=0;C[g]!=null;g++){if(C[g]&&(C[g]===true||C[g].nodeType===1&&l.contains(j,C[g])))o.push(x[g])}else for(g=0;C[g]!=null;g++)C[g]&&C[g].nodeType===1&&o.push(x[g]);else o.push.apply(o,C);else D(C,o);if(P){l(P,p,o,m);l.uniqueSort(o)}return o};l.uniqueSort=function(g){if(w){h= +k;g.sort(w);if(h)for(var j=1;j0};l.find=function(g,j,o){var m;if(!g)return[];for(var p=0,q=n.order.length;p":function(g,j){var o=typeof j==="string",m,p=0,q=g.length;if(o&&!/\W/.test(j))for(j=j.toLowerCase();p=0))o||m.push(t);else if(o)j[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var j=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=j[1]+(j[2]||1)-0;g[3]=j[3]-0}g[0]=e++;return g},ATTR:function(g,j,o, +m,p,q){j=g[1].replace(/\\/g,"");if(!q&&n.attrMap[j])g[1]=n.attrMap[j];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,j,o,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=l(g[3],null,null,j);else{g=l.filter(g[3],j,o,true^p);o||m.push.apply(m,g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== +true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,j,o){return!!l(o[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== +g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,j){return j===0},last:function(g,j,o,m){return j===m.length-1},even:function(g,j){return j%2===0},odd:function(g,j){return j%2===1},lt:function(g,j,o){return jo[3]-0},nth:function(g,j,o){return o[3]- +0===j},eq:function(g,j,o){return o[3]-0===j}},filter:{PSEUDO:function(g,j,o,m){var p=j[1],q=n.filters[p];if(q)return q(g,o,j,m);else if(p==="contains")return(g.textContent||g.innerText||l.getText([g])||"").indexOf(j[3])>=0;else if(p==="not"){j=j[3];o=0;for(m=j.length;o=0}},ID:function(g,j){return g.nodeType===1&&g.getAttribute("id")===j},TAG:function(g,j){return j==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== +j},CLASS:function(g,j){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(j)>-1},ATTR:function(g,j){var o=j[1];o=n.attrHandle[o]?n.attrHandle[o](g):g[o]!=null?g[o]:g.getAttribute(o);var m=o+"",p=j[2],q=j[4];return o==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&o!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,j,o,m){var p=n.setFilters[j[2]]; +if(p)return p(g,o,j,m)}}},s=n.match.POS,v=function(g,j){return"\\"+(j-0+1)},B;for(B in n.match){n.match[B]=RegExp(n.match[B].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[B]=RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[B].source.replace(/\\(\d+)/g,v))}var D=function(g,j){g=Array.prototype.slice.call(g,0);if(j){j.push.apply(j,g);return j}return g};try{Array.prototype.slice.call(u.documentElement.childNodes,0)}catch(H){D=function(g,j){var o=j||[],m=0;if(f.call(g)==="[object Array]")Array.prototype.push.apply(o, +g);else if(typeof g.length==="number")for(var p=g.length;m";var o=u.documentElement;o.insertBefore(g,o.firstChild);if(u.getElementById(j)){n.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:A:[]};n.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}o.removeChild(g); +o=g=null})();(function(){var g=u.createElement("div");g.appendChild(u.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(j,o){var m=o.getElementsByTagName(j[1]);if(j[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(j){return j.getAttribute("href",2)};g=null})();u.querySelectorAll&& +function(){var g=l,j=u.createElement("div");j.innerHTML="

";if(!(j.querySelectorAll&&j.querySelectorAll(".TEST").length===0)){l=function(m,p,q,t){p=p||u;if(!t&&!l.isXML(p))if(p.nodeType===9)try{return D(p.querySelectorAll(m),q)}catch(x){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var C=p.id,P=p.id="__sizzle__";try{return D(p.querySelectorAll("#"+P+" "+m),q)}catch(N){}finally{if(C)p.id=C;else p.removeAttribute("id")}}return g(m,p,q,t)};for(var o in g)l[o]=g[o]; +j=null}}();(function(){var g=u.documentElement,j=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,o=false;try{j.call(u.documentElement,":sizzle")}catch(m){o=true}if(j)l.matchesSelector=function(p,q){try{if(o||!n.match.PSEUDO.test(q))return j.call(p,q)}catch(t){}return l(q,null,null,[p]).length>0}})();(function(){var g=u.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length=== +0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(j,o,m){if(typeof o.getElementsByClassName!=="undefined"&&!m)return o.getElementsByClassName(j[1])};g=null}}})();l.contains=u.documentElement.contains?function(g,j){return g!==j&&(g.contains?g.contains(j):true)}:function(g,j){return!!(g.compareDocumentPosition(j)&16)};l.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var M=function(g, +j){for(var o=[],m="",p,q=j.nodeType?[j]:j;p=n.match.PSEUDO.exec(g);){m+=p[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;p=0;for(var t=q.length;p0)for(var h=d;h0},closest:function(a, +b){var d=[],e,f,h=this[0];if(c.isArray(a)){var k={},l,n=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:n})}h=h.parentNode;n++}}return d}k=$a.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h|| +!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context):c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}}); +c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling", +d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Wa.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||Ya.test(e))&&Xa.test(a))f=f.reverse();return this.pushStack(f,a,Za.call(arguments).join(","))}}); +c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===A||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var xa=/ jQuery\d+="(?:\d+|null)"/g, +$=/^\s+/,ya=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,za=/<([\w:]+)/,ab=/\s]+\/)>/g,O={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"], +area:[1,"",""],_default:[0,"",""]};O.optgroup=O.option;O.tbody=O.tfoot=O.colgroup=O.caption=O.thead;O.th=O.td;if(!c.support.htmlSerialize)O._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==A)return this.empty().append((this[0]&&this[0].ownerDocument||u).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this, +d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})}, +unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a= +c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*")); +c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(xa,"").replace(cb,'="$1">').replace($, +"")],e)[0]}else return this.cloneNode(true)});if(a===true){la(this,b);la(this.find("*"),b.find("*"))}return b},html:function(a){if(a===A)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(xa,""):null;else if(typeof a==="string"&&!Aa.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!O[(za.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(ya,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?l.cloneNode(true):l)}k.length&&c.each(k,Ka)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:u;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===u&&!Aa.test(a[0])&&(c.support.checkClone|| +!Ba.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h= +d.length;f0?this.clone(true):this).get();c(d[f])[b](k);e=e.concat(k)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||u;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||u;for(var f=[],h=0,k;(k=a[h])!=null;h++){if(typeof k==="number")k+="";if(k){if(typeof k==="string"&&!bb.test(k))k=b.createTextNode(k);else if(typeof k==="string"){k=k.replace(ya,"<$1>");var l=(za.exec(k)||["",""])[1].toLowerCase(),n=O[l]||O._default, +s=n[0],v=b.createElement("div");for(v.innerHTML=n[1]+k+n[2];s--;)v=v.lastChild;if(!c.support.tbody){s=ab.test(k);l=l==="table"&&!s?v.firstChild&&v.firstChild.childNodes:n[1]===""&&!s?v.childNodes:[];for(n=l.length-1;n>=0;--n)c.nodeName(l[n],"tbody")&&!l[n].childNodes.length&&l[n].parentNode.removeChild(l[n])}!c.support.leadingWhitespace&&$.test(k)&&v.insertBefore(b.createTextNode($.exec(k)[0]),v.firstChild);k=v.childNodes}if(k.nodeType)f.push(k);else f=c.merge(f,k)}}if(d)for(h=0;f[h];h++)if(e&& +c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script"))));d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,k=0,l;(l=a[k])!=null;k++)if(!(l.nodeName&&c.noData[l.nodeName.toLowerCase()]))if(d=l[c.expando]){if((b=e[d])&&b.events)for(var n in b.events)f[n]? +c.event.remove(l,n):c.removeEvent(l,n,b.handle);if(h)delete l[c.expando];else l.removeAttribute&&l.removeAttribute(c.expando);delete e[d]}}});var Ca=/alpha\([^)]*\)/i,db=/opacity=([^)]*)/,eb=/-([a-z])/ig,fb=/([A-Z])/g,Da=/^-?\d+(?:px)?$/i,gb=/^-?\d/,hb={position:"absolute",visibility:"hidden",display:"block"},La=["Left","Right"],Ma=["Top","Bottom"],W,ib=u.defaultView&&u.defaultView.getComputedStyle,jb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===A)return this; +return c.access(this,a,b,true,function(d,e,f){return f!==A?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true,zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),k=a.style,l=c.cssHooks[h];b=c.cssProps[h]|| +h;if(d!==A){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!l||!("set"in l)||(d=l.set(a,d))!==A)try{k[b]=d}catch(n){}}}else{if(l&&"get"in l&&(f=l.get(a,false,e))!==A)return f;return k[b]}}},css:function(a,b,d){var e,f=c.camelCase(b),h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==A)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]= +e[f]},camelCase:function(a){return a.replace(eb,jb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=ma(d,b,f);else c.swap(d,hb,function(){h=ma(d,b,f)});return h+"px"}},set:function(d,e){if(Da.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return db.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"": +b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f=d.filter||"";d.filter=Ca.test(f)?f.replace(Ca,e):d.filter+" "+e}};if(ib)W=function(a,b,d){var e;d=d.replace(fb,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return A;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};else if(u.documentElement.currentStyle)W=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b], +h=a.style;if(!Da.test(f)&&gb.test(f)){d=h.left;e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f};if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var kb=c.now(),lb=/)<[^<]*)*<\/script>/gi, +mb=/^(?:select|textarea)/i,nb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,ob=/^(?:GET|HEAD|DELETE)$/,Na=/\[\]$/,T=/\=\?(&|$)/,ia=/\?/,pb=/([?&])_=[^&]*/,qb=/^(\w+:)?\/\/([^\/?#]+)/,rb=/%20/g,sb=/#.*$/,Ea=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ea)return Ea.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d= +b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(k,l){if(l==="success"||l==="notmodified")h.html(f?c("
").append(k.responseText.replace(lb,"")).find(f):k.responseText);d&&h.each(d,[k.responseText,l,k])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& +!this.disabled&&(this.checked||mb.test(this.nodeName)||nb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, +getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", +script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),k=ob.test(h);b.url=b.url.replace(sb,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ia.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| +!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+kb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var l=E[d];E[d]=function(m){f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);if(c.isFunction(l))l(m);else{E[d]=A;try{delete E[d]}catch(p){}}v&&v.removeChild(B)}}if(b.dataType==="script"&&b.cache===null)b.cache= +false;if(b.cache===false&&h==="GET"){var n=c.now(),s=b.url.replace(pb,"$1_="+n);b.url=s+(s===b.url?(ia.test(b.url)?"&":"?")+"_="+n:"")}if(b.data&&h==="GET")b.url+=(ia.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");n=(n=qb.exec(b.url))&&(n[1]&&n[1]!==location.protocol||n[2]!==location.host);if(b.dataType==="script"&&h==="GET"&&n){var v=u.getElementsByTagName("head")[0]||u.documentElement,B=u.createElement("script");if(b.scriptCharset)B.charset=b.scriptCharset;B.src= +b.url;if(!d){var D=false;B.onload=B.onreadystatechange=function(){if(!D&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){D=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);B.onload=B.onreadystatechange=null;v&&B.parentNode&&v.removeChild(B)}}}v.insertBefore(B,v.firstChild);return A}var H=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!k||a&&a.contentType)w.setRequestHeader("Content-Type", +b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}n||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(G){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& +c.triggerGlobal(b,"ajaxSend",[w,b]);var M=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){H||c.handleComplete(b,w,e,f);H=true;if(w)w.onreadystatechange=c.noop}else if(!H&&w&&(w.readyState===4||m==="timeout")){H=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| +c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&g.call&&g.call(w);M("abort")}}catch(j){}b.async&&b.timeout>0&&setTimeout(function(){w&&!H&&M("timeout")},b.timeout);try{w.send(k||b.data==null?null:b.data)}catch(o){c.handleError(b,w,null,o);c.handleComplete(b,w,e,f)}b.async||M();return w}},param:function(a,b){var d=[],e=function(h,k){k=c.isFunction(k)?k():k;d[d.length]=encodeURIComponent(h)+ +"="+encodeURIComponent(k)};if(b===A)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)ca(f,a[f],b,e);return d.join("&").replace(rb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess",[b,a])},handleComplete:function(a, +b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),e=a.getResponseHeader("Etag"); +if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}});if(E.ActiveXObject)c.ajaxSettings.xhr= +function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var da={},tb=/^(?:toggle|show|hide)$/,ub=/^([+\-]=)?([\d+.\-]+)(.*)$/,aa,na=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show",3),a,b,d);else{a= +0;for(b=this.length;a=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, +d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* +Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(h){return f.step(h)} +this.startTime=c.now();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var f=this;a=c.fx;e.elem=this.elem;if(e()&&c.timers.push(e)&&!aa)aa=setInterval(a.tick,a.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; +this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(l,n){f.style["overflow"+n]=h.overflow[l]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| +this.options.show)for(var k in this.options.curAnim)c.style(this.elem,k,this.options.orig[k]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= +c.timers,b=0;b-1;e={};var s={};if(n)s=f.position();k=n?s.top:parseInt(k,10)||0;l=n?s.left:parseInt(l,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+k;if(b.left!=null)e.left=b.left-h.left+l;"using"in b?b.using.call(a, +e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Fa.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||u.body;a&&!Fa.test(a.nodeName)&& +c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==A)return this.each(function(){if(h=ea(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=ea(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); +c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(h){var k=c(this);k[d](e.call(this,h,k[d]()))});return c.isWindow(f)?f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b]:f.nodeType===9?Math.max(f.documentElement["client"+ +b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]):e===A?parseFloat(c.css(f,d)):this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/coverage/htmlfiles/jquery.hotkeys.js b/coverage/htmlfiles/jquery.hotkeys.js new file mode 100644 index 0000000..09b21e0 --- /dev/null +++ b/coverage/htmlfiles/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/coverage/htmlfiles/jquery.isonscreen.js b/coverage/htmlfiles/jquery.isonscreen.js new file mode 100644 index 0000000..0182ebd --- /dev/null +++ b/coverage/htmlfiles/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/coverage/htmlfiles/jquery.tablesorter.min.js b/coverage/htmlfiles/jquery.tablesorter.min.js new file mode 100644 index 0000000..64c7007 --- /dev/null +++ b/coverage/htmlfiles/jquery.tablesorter.min.js @@ -0,0 +1,2 @@ + +(function($){$.extend({tablesorter:new function(){var parsers=[],widgets=[];this.defaults={cssHeader:"header",cssAsc:"headerSortUp",cssDesc:"headerSortDown",sortInitialOrder:"asc",sortMultiSortKey:"shiftKey",sortForce:null,sortAppend:null,textExtraction:"simple",parsers:{},widgets:[],widgetZebra:{css:["even","odd"]},headers:{},widthFixed:false,cancelSelection:true,sortList:[],headerList:[],dateFormat:"us",decimal:'.',debug:false};function benchmark(s,d){log(s+","+(new Date().getTime()-d.getTime())+"ms");}this.benchmark=benchmark;function log(s){if(typeof console!="undefined"&&typeof console.debug!="undefined"){console.log(s);}else{alert(s);}}function buildParserCache(table,$headers){if(table.config.debug){var parsersDebug="";}var rows=table.tBodies[0].rows;if(table.tBodies[0].rows[0]){var list=[],cells=rows[0].cells,l=cells.length;for(var i=0;i1){arr=arr.concat(checkCellColSpan(table,headerArr,row++));}else{if(table.tHead.length==1||(cell.rowSpan>1||!r[row+1])){arr.push(cell);}}}return arr;};function checkHeaderMetadata(cell){if(($.metadata)&&($(cell).metadata().sorter===false)){return true;};return false;}function checkHeaderOptions(table,i){if((table.config.headers[i])&&(table.config.headers[i].sorter===false)){return true;};return false;}function applyWidget(table){var c=table.config.widgets;var l=c.length;for(var i=0;i');$("tr:first td",table.tBodies[0]).each(function(){colgroup.append($('
').css('width',$(this).width()));});$(table).prepend(colgroup);};}function updateHeaderSortCount(table,sortList){var c=table.config,l=sortList.length;for(var i=0;ib)?1:0));};function sortTextDesc(a,b){return((ba)?1:0));};function sortNumeric(a,b){return a-b;};function sortNumericDesc(a,b){return b-a;};function getCachedSortType(parsers,i){return parsers[i].type;};this.construct=function(settings){return this.each(function(){if(!this.tHead||!this.tBodies)return;var $this,$document,$headers,cache,config,shiftDown=0,sortOrder;this.config={};config=$.extend(this.config,$.tablesorter.defaults,settings);$this=$(this);$headers=buildHeaders(this);this.config.parsers=buildParserCache(this,$headers);cache=buildCache(this);var sortCSS=[config.cssDesc,config.cssAsc];fixColumnWidth(this);$headers.click(function(e){$this.trigger("sortStart");var totalRows=($this[0].tBodies[0]&&$this[0].tBodies[0].rows.length)||0;if(!this.sortDisabled&&totalRows>0){var $cell=$(this);var i=this.column;this.order=this.count++%2;if(!e[config.sortMultiSortKey]){config.sortList=[];if(config.sortForce!=null){var a=config.sortForce;for(var j=0;j0){$this.trigger("sorton",[config.sortList]);}applyWidget(this);});};this.addParser=function(parser){var l=parsers.length,a=true;for(var i=0;iD6{MWQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfZ8lau9@bl*u7(4JIy_w*Lo808 zo$Afkpupp@{Fv_bobxQ#pD>iB3oNa1d9=pM`D99*FvsH{pKJfpB1-4UD;=6}F=+gKX>Gx9b=!>PY1_pdfo@{(boFyt=akR{ E04sl8JOBUy literal 0 HcmV?d00001 diff --git a/coverage/htmlfiles/keybd_open.png b/coverage/htmlfiles/keybd_open.png new file mode 100644 index 0000000000000000000000000000000000000000..a77961db5424cfff43a63d399972ee85fc0dfdb1 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^%0SG+!3HE>D6{MWQjEnx?oJHr&dIz4a@dl*-CY>| zgW!U_%O?XxI14-?iy0WWg+Z8+Vb&Z8pdfpRr>`sfZ8lau9%kc-1xY}mZci7-5R21$ zCp+>TR^VYdE*ieC^FGV{Cyeh_21=Rotz3KNq=!VmdK II;Vst00jnQH~;_u literal 0 HcmV?d00001 diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html new file mode 100644 index 0000000..525939f --- /dev/null +++ b/coverage/htmlfiles/pyfile.html @@ -0,0 +1,90 @@ + + + + + {# 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 {{cu.name|escape}}: {{nums.pc_covered_str}}% + + {% if extra_css %} + + {% endif %} + + + + + + + + + + +
+ +

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 +

+
+
+ +
+
+ + + + +
+ {% for line in lines %} +

{{line.number}}

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

{% if line.annotate %}{{line.annotate}}{% endif %}{{line.html}} 

+ {% endfor %} +
+ + + + + + diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css new file mode 100644 index 0000000..811c640 --- /dev/null +++ b/coverage/htmlfiles/style.css @@ -0,0 +1,300 @@ +/* CSS styles for Coverage. */ +/* Page-wide styles */ +html, body, h1, h2, h3, p, 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; + } + +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: "courier new", 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; +} + +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; + cursor: pointer; +} + +.help_panel { + position: absolute; + background: #ffc; + 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: nowrap; + } + +.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; + font-style: italic; + color: #666; + float: right; + padding-right: .5em; + } +.text p.hide_par span.annotate { + display: none; + } + +/* 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 td.name a:hover { + text-decoration: underline; + color: #000; + } +#index tr.total { + } +#index tr.total td { + font-weight: bold; + border-top: 1px solid #ccc; + border-bottom: none; + } +#index tr.file:hover { + background: #eeeeee; + } diff --git a/coverage/misc.py b/coverage/misc.py new file mode 100644 index 0000000..ece8023 --- /dev/null +++ b/coverage/misc.py @@ -0,0 +1,159 @@ +"""Miscellaneous stuff for Coverage.""" + +import errno +import inspect +import os +import sys + +from coverage.backward import md5, sorted # pylint: disable=W0622 +from coverage.backward import string_class, to_bytes + + +def nice_pair(pair): + """Make a nice string representation of a pair of numbers. + + If the numbers are equal, just return the number, otherwise return the pair + with a dash between them, indicating the range. + + """ + start, end = pair + if start == end: + return "%d" % start + else: + return "%d-%d" % (start, end) + + +def format_lines(statements, lines): + """Nicely format a list of line numbers. + + Format a list of line numbers for printing by coalescing groups of lines as + long as the lines represent consecutive statements. This will coalesce + even if there are gaps between statements. + + For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and + `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14". + + """ + pairs = [] + i = 0 + j = 0 + start = None + while i < len(statements) and j < len(lines): + if statements[i] == lines[j]: + if start == None: + start = lines[j] + end = lines[j] + j += 1 + elif start: + pairs.append((start, end)) + start = None + i += 1 + if start: + pairs.append((start, end)) + ret = ', '.join(map(nice_pair, pairs)) + return ret + + +def short_stack(): + """Return a string summarizing the call stack.""" + stack = inspect.stack()[:0:-1] + return "\n".join(["%30s : %s @%d" % (t[3],t[1],t[2]) for t in stack]) + + +def expensive(fn): + """A decorator to cache the result of an expensive operation. + + Only applies to methods with no arguments. + + """ + attr = "_cache_" + fn.__name__ + def _wrapped(self): + """Inner fn that checks the cache.""" + if not hasattr(self, attr): + setattr(self, attr, fn(self)) + return getattr(self, attr) + return _wrapped + + +def bool_or_none(b): + """Return bool(b), but preserve None.""" + if b is None: + return None + else: + return bool(b) + + +def join_regex(regexes): + """Combine a list of regexes into one that matches any of them.""" + if len(regexes) > 1: + return "(" + ")|(".join(regexes) + ")" + elif regexes: + return regexes[0] + else: + return "" + + +def file_be_gone(path): + """Remove a file, and don't get annoyed if it doesn't exist.""" + try: + os.remove(path) + except OSError: + _, e, _ = sys.exc_info() + if e.errno != errno.ENOENT: + raise + + +class Hasher(object): + """Hashes Python data into md5.""" + def __init__(self): + self.md5 = md5() + + def update(self, v): + """Add `v` to the hash, recursively if needed.""" + self.md5.update(to_bytes(str(type(v)))) + if isinstance(v, string_class): + self.md5.update(to_bytes(v)) + elif isinstance(v, (int, float)): + self.update(str(v)) + elif isinstance(v, (tuple, list)): + for e in v: + self.update(e) + elif isinstance(v, dict): + keys = v.keys() + for k in sorted(keys): + self.update(k) + self.update(v[k]) + else: + for k in dir(v): + if k.startswith('__'): + continue + a = getattr(v, k) + if inspect.isroutine(a): + continue + self.update(k) + self.update(a) + + def digest(self): + """Retrieve the digest of the hash.""" + return self.md5.digest() + + +class CoverageException(Exception): + """An exception specific to Coverage.""" + pass + +class NoSource(CoverageException): + """We couldn't find the source for a module.""" + pass + +class NotPython(CoverageException): + """A source file turned out not to be parsable Python.""" + pass + +class ExceptionDuringRun(CoverageException): + """An exception happened while running customer code. + + Construct it with three arguments, the values from `sys.exc_info`. + + """ + pass diff --git a/coverage/parser.py b/coverage/parser.py new file mode 100644 index 0000000..4445505 --- /dev/null +++ b/coverage/parser.py @@ -0,0 +1,662 @@ +"""Code parsing for Coverage.""" + +import opcode, re, sys, token, tokenize + +from coverage.backward import set, sorted, StringIO # pylint: disable=W0622 +from coverage.backward import open_source +from coverage.bytecode import ByteCodes, CodeObjects +from coverage.misc import nice_pair, expensive, join_regex +from coverage.misc import CoverageException, NoSource, NotPython + + +class CodeParser(object): + """Parse code to find executable lines, excluded lines, etc.""" + + def __init__(self, text=None, filename=None, exclude=None): + """ + Source can be provided as `text`, the text itself, or `filename`, from + which the text will be read. Excluded lines are those that match + `exclude`, a regex. + + """ + assert text or filename, "CodeParser needs either text or filename" + self.filename = filename or "" + self.text = text + if not self.text: + try: + sourcef = open_source(self.filename) + try: + self.text = sourcef.read() + finally: + sourcef.close() + except IOError: + _, err, _ = sys.exc_info() + raise NoSource( + "No source for code: '%s': %s" % (self.filename, err) + ) + + # Scrap the BOM if it exists. + if self.text and ord(self.text[0]) == 0xfeff: + self.text = self.text[1:] + + self.exclude = exclude + + self.show_tokens = False + + # The text lines of the parsed code. + self.lines = self.text.split('\n') + + # The line numbers of excluded lines of code. + self.excluded = set() + + # The line numbers of docstring lines. + self.docstrings = set() + + # The line numbers of class definitions. + self.classdefs = set() + + # A dict mapping line numbers to (lo,hi) for multi-line statements. + self.multiline = {} + + # The line numbers that start statements. + self.statement_starts = set() + + # Lazily-created ByteParser + self._byte_parser = None + + def _get_byte_parser(self): + """Create a ByteParser on demand.""" + if not self._byte_parser: + self._byte_parser = \ + ByteParser(text=self.text, filename=self.filename) + return self._byte_parser + byte_parser = property(_get_byte_parser) + + def lines_matching(self, *regexes): + """Find the lines matching one of a list of regexes. + + Returns a set of line numbers, the lines that contain a match for one + of the regexes in `regexes`. The entire line needn't match, just a + part of it. + + """ + regex_c = re.compile(join_regex(regexes)) + matches = set() + for i, ltext in enumerate(self.lines): + if regex_c.search(ltext): + matches.add(i+1) + return matches + + def _raw_parse(self): + """Parse the source to find the interesting facts about its lines. + + A handful of member fields are updated. + + """ + # Find lines which match an exclusion pattern. + if self.exclude: + self.excluded = self.lines_matching(self.exclude) + + # Tokenize, to find excluded suites, to find docstrings, and to find + # multi-line statements. + indent = 0 + exclude_indent = 0 + excluding = False + prev_toktype = token.INDENT + first_line = None + empty = True + + tokgen = tokenize.generate_tokens(StringIO(self.text).readline) + for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen: + if self.show_tokens: # pragma: not covered + print("%10s %5s %-20r %r" % ( + tokenize.tok_name.get(toktype, toktype), + nice_pair((slineno, elineno)), ttext, ltext + )) + if toktype == token.INDENT: + indent += 1 + elif toktype == token.DEDENT: + indent -= 1 + elif toktype == token.NAME and ttext == 'class': + # Class definitions look like branches in the byte code, so + # we need to exclude them. The simplest way is to note the + # lines with the 'class' keyword. + self.classdefs.add(slineno) + elif toktype == token.OP and ttext == ':': + if not excluding and elineno in self.excluded: + # Start excluding a suite. We trigger off of the colon + # token so that the #pragma comment will be recognized on + # the same line as the colon. + exclude_indent = indent + excluding = True + elif toktype == token.STRING and prev_toktype == token.INDENT: + # Strings that are first on an indented line are docstrings. + # (a trick from trace.py in the stdlib.) This works for + # 99.9999% of cases. For the rest (!) see: + # http://stackoverflow.com/questions/1769332/x/1769794#1769794 + for i in range(slineno, elineno+1): + self.docstrings.add(i) + elif toktype == token.NEWLINE: + if first_line is not None and elineno != first_line: + # We're at the end of a line, and we've ended on a + # different line than the first line of the statement, + # so record a multi-line range. + rng = (first_line, elineno) + for l in range(first_line, elineno+1): + self.multiline[l] = rng + first_line = None + + if ttext.strip() and toktype != tokenize.COMMENT: + # A non-whitespace token. + empty = False + if first_line is None: + # The token is not whitespace, and is the first in a + # statement. + first_line = slineno + # Check whether to end an excluded suite. + if excluding and indent <= exclude_indent: + excluding = False + if excluding: + self.excluded.add(elineno) + + prev_toktype = toktype + + # Find the starts of the executable statements. + if not empty: + self.statement_starts.update(self.byte_parser._find_statements()) + + def first_line(self, line): + """Return the first line number of the statement including `line`.""" + rng = self.multiline.get(line) + if rng: + first_line = rng[0] + else: + first_line = line + return first_line + + def first_lines(self, lines, ignore=None): + """Map the line numbers in `lines` to the correct first line of the + statement. + + Skip any line mentioned in `ignore`. + + Returns a sorted list of the first lines. + + """ + ignore = ignore or [] + lset = set() + for l in lines: + if l in ignore: + continue + new_l = self.first_line(l) + if new_l not in ignore: + lset.add(new_l) + return sorted(lset) + + def parse_source(self): + """Parse source text to find executable lines, excluded lines, etc. + + Return values are 1) a sorted list of executable line numbers, and + 2) a sorted list of excluded line numbers. + + Reported line numbers are normalized to the first line of multi-line + statements. + + """ + try: + self._raw_parse() + except (tokenize.TokenError, IndentationError): + _, tokerr, _ = sys.exc_info() + msg, lineno = tokerr.args + raise NotPython( + "Couldn't parse '%s' as Python source: '%s' at %s" % + (self.filename, msg, lineno) + ) + + excluded_lines = self.first_lines(self.excluded) + ignore = excluded_lines + list(self.docstrings) + lines = self.first_lines(self.statement_starts, ignore) + + return lines, excluded_lines + + def arcs(self): + """Get information about the arcs available in the code. + + Returns a sorted list of line number pairs. Line numbers have been + normalized to the first line of multiline statements. + + """ + all_arcs = [] + for l1, l2 in self.byte_parser._all_arcs(): + fl1 = self.first_line(l1) + fl2 = self.first_line(l2) + if fl1 != fl2: + all_arcs.append((fl1, fl2)) + return sorted(all_arcs) + arcs = expensive(arcs) + + def exit_counts(self): + """Get a mapping from line numbers to count of exits from that line. + + Excluded lines are excluded. + + """ + excluded_lines = self.first_lines(self.excluded) + exit_counts = {} + for l1, l2 in self.arcs(): + if l1 < 0: + # Don't ever report -1 as a line number + continue + if l1 in excluded_lines: + # Don't report excluded lines as line numbers. + continue + if l2 in excluded_lines: + # Arcs to excluded lines shouldn't count. + continue + if l1 not in exit_counts: + exit_counts[l1] = 0 + exit_counts[l1] += 1 + + # Class definitions have one extra exit, so remove one for each: + for l in self.classdefs: + # Ensure key is there: classdefs can include excluded lines. + if l in exit_counts: + exit_counts[l] -= 1 + + return exit_counts + exit_counts = expensive(exit_counts) + + +## Opcodes that guide the ByteParser. + +def _opcode(name): + """Return the opcode by name from the opcode module.""" + return opcode.opmap[name] + +def _opcode_set(*names): + """Return a set of opcodes by the names in `names`.""" + s = set() + for name in names: + try: + s.add(_opcode(name)) + except KeyError: + pass + return s + +# Opcodes that leave the code object. +OPS_CODE_END = _opcode_set('RETURN_VALUE') + +# Opcodes that unconditionally end the code chunk. +OPS_CHUNK_END = _opcode_set( + 'JUMP_ABSOLUTE', 'JUMP_FORWARD', 'RETURN_VALUE', 'RAISE_VARARGS', + 'BREAK_LOOP', 'CONTINUE_LOOP', + ) + +# Opcodes that unconditionally begin a new code chunk. By starting new chunks +# with unconditional jump instructions, we neatly deal with jumps to jumps +# properly. +OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD') + +# Opcodes that push a block on the block stack. +OPS_PUSH_BLOCK = _opcode_set( + 'SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY', 'SETUP_WITH' + ) + +# Block types for exception handling. +OPS_EXCEPT_BLOCKS = _opcode_set('SETUP_EXCEPT', 'SETUP_FINALLY') + +# Opcodes that pop a block from the block stack. +OPS_POP_BLOCK = _opcode_set('POP_BLOCK') + +# Opcodes that have a jump destination, but aren't really a jump. +OPS_NO_JUMP = OPS_PUSH_BLOCK + +# Individual opcodes we need below. +OP_BREAK_LOOP = _opcode('BREAK_LOOP') +OP_END_FINALLY = _opcode('END_FINALLY') +OP_COMPARE_OP = _opcode('COMPARE_OP') +COMPARE_EXCEPTION = 10 # just have to get this const from the code. +OP_LOAD_CONST = _opcode('LOAD_CONST') +OP_RETURN_VALUE = _opcode('RETURN_VALUE') + + +class ByteParser(object): + """Parse byte codes to understand the structure of code.""" + + def __init__(self, code=None, text=None, filename=None): + if code: + self.code = code + self.text = text + else: + if not text: + assert filename, "If no code or text, need a filename" + sourcef = open_source(filename) + try: + text = sourcef.read() + finally: + sourcef.close() + self.text = text + + try: + # Python 2.3 and 2.4 don't like partial last lines, so be sure + # the text ends nicely for them. + self.code = compile(text + '\n', filename, "exec") + except SyntaxError: + _, synerr, _ = sys.exc_info() + raise NotPython( + "Couldn't parse '%s' as Python source: '%s' at line %d" % + (filename, synerr.msg, synerr.lineno) + ) + + # Alternative Python implementations don't always provide all the + # attributes on code objects that we need to do the analysis. + for attr in ['co_lnotab', 'co_firstlineno', 'co_consts', 'co_code']: + if not hasattr(self.code, attr): + raise CoverageException( + "This implementation of Python doesn't support code " + "analysis.\n" + "Run coverage.py under CPython for this command." + ) + + def child_parsers(self): + """Iterate over all the code objects nested within this one. + + The iteration includes `self` as its first value. + + """ + children = CodeObjects(self.code) + return [ByteParser(code=c, text=self.text) for c in children] + + # Getting numbers from the lnotab value changed in Py3.0. + if sys.version_info >= (3, 0): + def _lnotab_increments(self, lnotab): + """Return a list of ints from the lnotab bytes in 3.x""" + return list(lnotab) + else: + def _lnotab_increments(self, lnotab): + """Return a list of ints from the lnotab string in 2.x""" + return [ord(c) for c in lnotab] + + def _bytes_lines(self): + """Map byte offsets to line numbers in `code`. + + Uses co_lnotab described in Python/compile.c to map byte offsets to + line numbers. Returns a list: [(b0, l0), (b1, l1), ...] + + """ + # Adapted from dis.py in the standard library. + byte_increments = self._lnotab_increments(self.code.co_lnotab[0::2]) + line_increments = self._lnotab_increments(self.code.co_lnotab[1::2]) + + bytes_lines = [] + last_line_num = None + line_num = self.code.co_firstlineno + byte_num = 0 + for byte_incr, line_incr in zip(byte_increments, line_increments): + if byte_incr: + if line_num != last_line_num: + bytes_lines.append((byte_num, line_num)) + last_line_num = line_num + byte_num += byte_incr + line_num += line_incr + if line_num != last_line_num: + bytes_lines.append((byte_num, line_num)) + return bytes_lines + + def _find_statements(self): + """Find the statements in `self.code`. + + Return a set of line numbers that start statements. Recurses into all + code objects reachable from `self.code`. + + """ + stmts = set() + for bp in self.child_parsers(): + # Get all of the lineno information from this code. + for _, l in bp._bytes_lines(): + stmts.add(l) + return stmts + + def _split_into_chunks(self): + """Split the code object into a list of `Chunk` objects. + + Each chunk is only entered at its first instruction, though there can + be many exits from a chunk. + + Returns a list of `Chunk` objects. + + """ + + # The list of chunks so far, and the one we're working on. + chunks = [] + chunk = None + bytes_lines_map = dict(self._bytes_lines()) + + # The block stack: loops and try blocks get pushed here for the + # implicit jumps that can occur. + # Each entry is a tuple: (block type, destination) + block_stack = [] + + # Some op codes are followed by branches that should be ignored. This + # is a count of how many ignores are left. + ignore_branch = 0 + + # We have to handle the last two bytecodes specially. + ult = penult = None + + for bc in ByteCodes(self.code.co_code): + # Maybe have to start a new chunk + if bc.offset in bytes_lines_map: + # Start a new chunk for each source line number. + if chunk: + chunk.exits.add(bc.offset) + chunk = Chunk(bc.offset, bytes_lines_map[bc.offset]) + chunks.append(chunk) + elif bc.op in OPS_CHUNK_BEGIN: + # Jumps deserve their own unnumbered chunk. This fixes + # problems with jumps to jumps getting confused. + if chunk: + chunk.exits.add(bc.offset) + chunk = Chunk(bc.offset) + chunks.append(chunk) + + if not chunk: + chunk = Chunk(bc.offset) + chunks.append(chunk) + + # Look at the opcode + if bc.jump_to >= 0 and bc.op not in OPS_NO_JUMP: + if ignore_branch: + # Someone earlier wanted us to ignore this branch. + ignore_branch -= 1 + else: + # The opcode has a jump, it's an exit for this chunk. + chunk.exits.add(bc.jump_to) + + if bc.op in OPS_CODE_END: + # The opcode can exit the code object. + chunk.exits.add(-self.code.co_firstlineno) + if bc.op in OPS_PUSH_BLOCK: + # The opcode adds a block to the block_stack. + block_stack.append((bc.op, bc.jump_to)) + if bc.op in OPS_POP_BLOCK: + # The opcode pops a block from the block stack. + block_stack.pop() + if bc.op in OPS_CHUNK_END: + # This opcode forces the end of the chunk. + if bc.op == OP_BREAK_LOOP: + # A break is implicit: jump where the top of the + # block_stack points. + chunk.exits.add(block_stack[-1][1]) + chunk = None + if bc.op == OP_END_FINALLY: + if block_stack: + # A break that goes through a finally will jump to whatever + # block is on top of the stack. + chunk.exits.add(block_stack[-1][1]) + # For the finally clause we need to find the closest exception + # block, and use its jump target as an exit. + for iblock in range(len(block_stack)-1, -1, -1): + if block_stack[iblock][0] in OPS_EXCEPT_BLOCKS: + chunk.exits.add(block_stack[iblock][1]) + break + if bc.op == OP_COMPARE_OP and bc.arg == COMPARE_EXCEPTION: + # This is an except clause. We want to overlook the next + # branch, so that except's don't count as branches. + ignore_branch += 1 + + penult = ult + ult = bc + + if chunks: + # The last two bytecodes could be a dummy "return None" that + # shouldn't be counted as real code. Every Python code object seems + # to end with a return, and a "return None" is inserted if there + # isn't an explicit return in the source. + if ult and penult: + if penult.op == OP_LOAD_CONST and ult.op == OP_RETURN_VALUE: + if self.code.co_consts[penult.arg] is None: + # This is "return None", but is it dummy? A real line + # would be a last chunk all by itself. + if chunks[-1].byte != penult.offset: + ex = -self.code.co_firstlineno + # Split the last chunk + last_chunk = chunks[-1] + last_chunk.exits.remove(ex) + last_chunk.exits.add(penult.offset) + chunk = Chunk(penult.offset) + chunk.exits.add(ex) + chunks.append(chunk) + + # Give all the chunks a length. + chunks[-1].length = bc.next_offset - chunks[-1].byte # pylint: disable=W0631,C0301 + for i in range(len(chunks)-1): + chunks[i].length = chunks[i+1].byte - chunks[i].byte + + return chunks + + def _arcs(self): + """Find the executable arcs in the code. + + Returns a set of pairs, (from,to). From and to are integer line + numbers. If from is < 0, then the arc is an entrance into the code + object. If to is < 0, the arc is an exit from the code object. + + """ + chunks = self._split_into_chunks() + + # A map from byte offsets to chunks jumped into. + byte_chunks = dict([(c.byte, c) for c in chunks]) + + # Build a map from byte offsets to actual lines reached. + byte_lines = {} + bytes_to_add = set([c.byte for c in chunks]) + + while bytes_to_add: + byte_to_add = bytes_to_add.pop() + if byte_to_add in byte_lines or byte_to_add < 0: + continue + + # Which lines does this chunk lead to? + bytes_considered = set() + bytes_to_consider = [byte_to_add] + lines = set() + + while bytes_to_consider: + byte = bytes_to_consider.pop() + bytes_considered.add(byte) + + # Find chunk for byte + try: + ch = byte_chunks[byte] + except KeyError: + for ch in chunks: + if ch.byte <= byte < ch.byte+ch.length: + break + else: + # No chunk for this byte! + raise Exception("Couldn't find chunk @ %d" % byte) + byte_chunks[byte] = ch # pylint: disable=W0631 + + if ch.line: + lines.add(ch.line) + else: + for ex in ch.exits: + if ex < 0: + lines.add(ex) + elif ex not in bytes_considered: + bytes_to_consider.append(ex) + + bytes_to_add.update(ch.exits) + + byte_lines[byte_to_add] = lines + + # Figure out for each chunk where the exits go. + arcs = set() + for chunk in chunks: + if chunk.line: + for ex in chunk.exits: + if ex < 0: + exit_lines = [ex] + else: + exit_lines = byte_lines[ex] + for exit_line in exit_lines: + if chunk.line != exit_line: + arcs.add((chunk.line, exit_line)) + for line in byte_lines[0]: + arcs.add((-1, line)) + + return arcs + + def _all_chunks(self): + """Returns a list of `Chunk` objects for this code and its children. + + See `_split_into_chunks` for details. + + """ + chunks = [] + for bp in self.child_parsers(): + chunks.extend(bp._split_into_chunks()) + + return chunks + + def _all_arcs(self): + """Get the set of all arcs in this code object and its children. + + See `_arcs` for details. + + """ + arcs = set() + for bp in self.child_parsers(): + arcs.update(bp._arcs()) + + return arcs + + +class Chunk(object): + """A sequence of bytecodes with a single entrance. + + To analyze byte code, we have to divide it into chunks, sequences of byte + codes such that each basic block has only one entrance, the first + instruction in the block. + + This is almost the CS concept of `basic block`_, except that we're willing + to have many exits from a chunk, and "basic block" is a more cumbersome + term. + + .. _basic block: http://en.wikipedia.org/wiki/Basic_block + + An exit < 0 means the chunk can leave the code (return). The exit is + the negative of the starting line number of the code block. + + """ + def __init__(self, byte, line=0): + self.byte = byte + self.line = line + self.length = 0 + self.exits = set() + + def __repr__(self): + return "<%d+%d @%d %r>" % ( + self.byte, self.length, self.line, list(self.exits) + ) diff --git a/coverage/phystokens.py b/coverage/phystokens.py new file mode 100644 index 0000000..166020e --- /dev/null +++ b/coverage/phystokens.py @@ -0,0 +1,206 @@ +"""Better tokenizing for coverage.py.""" + +import codecs, keyword, re, sys, token, tokenize +from coverage.backward import StringIO # pylint: disable=W0622 + +def phys_tokens(toks): + """Return all physical tokens, even line continuations. + + tokenize.generate_tokens() doesn't return a token for the backslash that + continues lines. This wrapper provides those tokens so that we can + re-create a faithful representation of the original source. + + Returns the same values as generate_tokens() + + """ + last_line = None + last_lineno = -1 + last_ttype = None + for ttype, ttext, (slineno, scol), (elineno, ecol), ltext in toks: + if last_lineno != elineno: + if last_line and last_line[-2:] == "\\\n": + # We are at the beginning of a new line, and the last line + # ended with a backslash. We probably have to inject a + # backslash token into the stream. Unfortunately, there's more + # to figure out. This code:: + # + # usage = """\ + # HEY THERE + # """ + # + # triggers this condition, but the token text is:: + # + # '"""\\\nHEY THERE\n"""' + # + # so we need to figure out if the backslash is already in the + # string token or not. + inject_backslash = True + if last_ttype == tokenize.COMMENT: + # Comments like this \ + # should never result in a new token. + inject_backslash = False + elif ttype == token.STRING: + if "\n" in ttext and ttext.split('\n', 1)[0][-1] == '\\': + # It's a multiline string and the first line ends with + # a backslash, so we don't need to inject another. + inject_backslash = False + if inject_backslash: + # Figure out what column the backslash is in. + ccol = len(last_line.split("\n")[-2]) - 1 + # Yield the token, with a fake token type. + yield ( + 99999, "\\\n", + (slineno, ccol), (slineno, ccol+2), + last_line + ) + last_line = ltext + last_ttype = ttype + yield ttype, ttext, (slineno, scol), (elineno, ecol), ltext + last_lineno = elineno + + +def source_token_lines(source): + """Generate a series of lines, one for each line in `source`. + + Each line is a list of pairs, each pair is a token:: + + [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ] + + Each pair has a token class, and the token text. + + If you concatenate all the token texts, and then join them with newlines, + you should have your original `source` back, with two differences: + trailing whitespace is not preserved, and a final line with no newline + is indistinguishable from a final line with a newline. + + """ + ws_tokens = [token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL] + line = [] + col = 0 + source = source.expandtabs(8).replace('\r\n', '\n') + tokgen = tokenize.generate_tokens(StringIO(source).readline) + for ttype, ttext, (_, scol), (_, ecol), _ in phys_tokens(tokgen): + mark_start = True + for part in re.split('(\n)', ttext): + if part == '\n': + yield line + line = [] + col = 0 + mark_end = False + elif part == '': + mark_end = False + elif ttype in ws_tokens: + mark_end = False + else: + if mark_start and scol > col: + line.append(("ws", " " * (scol - col))) + mark_start = False + tok_class = tokenize.tok_name.get(ttype, 'xx').lower()[:3] + if ttype == token.NAME and keyword.iskeyword(ttext): + tok_class = "key" + line.append((tok_class, part)) + mark_end = True + scol = 0 + if mark_end: + col = ecol + + if line: + yield line + +def source_encoding(source): + """Determine the encoding for `source` (a string), according to PEP 263. + + Returns a string, the name of the encoding. + + """ + # Note: this function should never be called on Python 3, since py3 has + # built-in tools to do this. + assert sys.version_info < (3, 0) + + # This is mostly code adapted from Py3.2's tokenize module. + + cookie_re = re.compile(r"coding[:=]\s*([-\w.]+)") + + # Do this so the detect_encode code we copied will work. + readline = iter(source.splitlines()).next + + def _get_normal_name(orig_enc): + """Imitates get_normal_name in tokenizer.c.""" + # Only care about the first 12 characters. + enc = orig_enc[:12].lower().replace("_", "-") + if re.match(r"^utf-8($|-)", enc): + return "utf-8" + if re.match(r"^(latin-1|iso-8859-1|iso-latin-1)($|-)", enc): + return "iso-8859-1" + return orig_enc + + # From detect_encode(): + # It detects the encoding from the presence of a utf-8 bom or an encoding + # cookie as specified in pep-0263. If both a bom and a cookie are present, + # but disagree, a SyntaxError will be raised. If the encoding cookie is an + # invalid charset, raise a SyntaxError. Note that if a utf-8 bom is found, + # 'utf-8-sig' is returned. + + # If no encoding is specified, then the default will be returned. The + # default varied with version. + + if sys.version_info <= (2, 4): + default = 'iso-8859-1' + else: + default = 'ascii' + + bom_found = False + encoding = None + + def read_or_stop(): + """Get the next source line, or ''.""" + try: + return readline() + except StopIteration: + return '' + + def find_cookie(line): + """Find an encoding cookie in `line`.""" + try: + line_string = line.decode('ascii') + except UnicodeDecodeError: + return None + + matches = cookie_re.findall(line_string) + if not matches: + return None + encoding = _get_normal_name(matches[0]) + try: + codec = codecs.lookup(encoding) + except LookupError: + # This behaviour mimics the Python interpreter + raise SyntaxError("unknown encoding: " + encoding) + + if bom_found: + if codec.name != 'utf-8': + # This behaviour mimics the Python interpreter + raise SyntaxError('encoding problem: utf-8') + encoding += '-sig' + return encoding + + first = read_or_stop() + if first.startswith(codecs.BOM_UTF8): + bom_found = True + first = first[3:] + default = 'utf-8-sig' + if not first: + return default + + encoding = find_cookie(first) + if encoding: + return encoding + + second = read_or_stop() + if not second: + return default + + encoding = find_cookie(second) + if encoding: + return encoding + + return default diff --git a/coverage/report.py b/coverage/report.py new file mode 100644 index 0000000..34f4442 --- /dev/null +++ b/coverage/report.py @@ -0,0 +1,92 @@ +"""Reporter foundation for Coverage.""" + +import fnmatch, os +from coverage.codeunit import code_unit_factory +from coverage.files import prep_patterns +from coverage.misc import CoverageException, NoSource, NotPython + +class Reporter(object): + """A base class for all reporters.""" + + def __init__(self, coverage, config): + """Create a reporter. + + `coverage` is the coverage instance. `config` is an instance of + CoverageConfig, for controlling all sorts of behavior. + + """ + self.coverage = coverage + self.config = config + + # The code units to report on. Set by find_code_units. + self.code_units = [] + + # The directory into which to place the report, used by some derived + # classes. + self.directory = None + + def find_code_units(self, morfs): + """Find the code units we'll report on. + + `morfs` is a list of modules or filenames. + + """ + morfs = morfs or self.coverage.data.measured_files() + file_locator = self.coverage.file_locator + self.code_units = code_unit_factory(morfs, file_locator) + + if self.config.include: + patterns = prep_patterns(self.config.include) + filtered = [] + for cu in self.code_units: + for pattern in patterns: + if fnmatch.fnmatch(cu.filename, pattern): + filtered.append(cu) + break + self.code_units = filtered + + if self.config.omit: + patterns = prep_patterns(self.config.omit) + filtered = [] + for cu in self.code_units: + for pattern in patterns: + if fnmatch.fnmatch(cu.filename, pattern): + break + else: + filtered.append(cu) + self.code_units = filtered + + self.code_units.sort() + + def report_files(self, report_fn, morfs, directory=None): + """Run a reporting function on a number of morfs. + + `report_fn` is called for each relative morf in `morfs`. It is called + as:: + + report_fn(code_unit, analysis) + + where `code_unit` is the `CodeUnit` for the morf, and `analysis` is + the `Analysis` for the morf. + + """ + self.find_code_units(morfs) + + if not self.code_units: + raise CoverageException("No data to report.") + + self.directory = directory + if self.directory and not os.path.exists(self.directory): + os.makedirs(self.directory) + + for cu in self.code_units: + try: + report_fn(cu, self.coverage._analyze(cu)) + except NoSource: + if not self.config.ignore_errors: + raise + except NotPython: + # Only report errors for .py files, and only if we didn't + # explicitly suppress those errors. + if cu.should_be_python() and not self.config.ignore_errors: + raise diff --git a/coverage/results.py b/coverage/results.py new file mode 100644 index 0000000..77ff2a2 --- /dev/null +++ b/coverage/results.py @@ -0,0 +1,254 @@ +"""Results of coverage measurement.""" + +import os + +from coverage.backward import iitems, set, sorted # pylint: disable=W0622 +from coverage.misc import format_lines, join_regex, NoSource +from coverage.parser import CodeParser + + +class Analysis(object): + """The results of analyzing a code unit.""" + + def __init__(self, cov, code_unit): + self.coverage = cov + self.code_unit = code_unit + + self.filename = self.code_unit.filename + ext = os.path.splitext(self.filename)[1] + source = None + if ext == '.py': + if not os.path.exists(self.filename): + source = self.coverage.file_locator.get_zip_data(self.filename) + if not source: + raise NoSource("No source for code: '%s'" % self.filename) + + self.parser = CodeParser( + text=source, filename=self.filename, + exclude=self.coverage._exclude_regex('exclude') + ) + self.statements, self.excluded = self.parser.parse_source() + + # Identify missing statements. + executed = self.coverage.data.executed_lines(self.filename) + exec1 = self.parser.first_lines(executed) + self.missing = sorted(set(self.statements) - set(exec1)) + + if self.coverage.data.has_arcs(): + self.no_branch = self.parser.lines_matching( + join_regex(self.coverage.config.partial_list), + join_regex(self.coverage.config.partial_always_list) + ) + n_branches = self.total_branches() + mba = self.missing_branch_arcs() + n_partial_branches = sum( + [len(v) for k,v in iitems(mba) if k not in self.missing] + ) + n_missing_branches = sum([len(v) for k,v in iitems(mba)]) + else: + n_branches = n_partial_branches = n_missing_branches = 0 + self.no_branch = set() + + self.numbers = Numbers( + n_files=1, + n_statements=len(self.statements), + n_excluded=len(self.excluded), + n_missing=len(self.missing), + n_branches=n_branches, + n_partial_branches=n_partial_branches, + n_missing_branches=n_missing_branches, + ) + + def missing_formatted(self): + """The missing line numbers, formatted nicely. + + Returns a string like "1-2, 5-11, 13-14". + + """ + return format_lines(self.statements, self.missing) + + def has_arcs(self): + """Were arcs measured in this result?""" + return self.coverage.data.has_arcs() + + def arc_possibilities(self): + """Returns a sorted list of the arcs in the code.""" + arcs = self.parser.arcs() + return arcs + + def arcs_executed(self): + """Returns a sorted list of the arcs actually executed in the code.""" + executed = self.coverage.data.executed_arcs(self.filename) + m2fl = self.parser.first_line + executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed] + return sorted(executed) + + def arcs_missing(self): + """Returns a sorted list of the arcs in the code not executed.""" + possible = self.arc_possibilities() + executed = self.arcs_executed() + missing = [ + p for p in possible + if p not in executed + and p[0] not in self.no_branch + ] + return sorted(missing) + + def arcs_unpredicted(self): + """Returns a sorted list of the executed arcs missing from the code.""" + possible = self.arc_possibilities() + executed = self.arcs_executed() + # Exclude arcs here which connect a line to itself. They can occur + # in executed data in some cases. This is where they can cause + # trouble, and here is where it's the least burden to remove them. + unpredicted = [ + e for e in executed + if e not in possible + and e[0] != e[1] + ] + return sorted(unpredicted) + + def branch_lines(self): + """Returns a list of line numbers that have more than one exit.""" + exit_counts = self.parser.exit_counts() + return [l1 for l1,count in iitems(exit_counts) if count > 1] + + def total_branches(self): + """How many total branches are there?""" + exit_counts = self.parser.exit_counts() + return sum([count for count in exit_counts.values() if count > 1]) + + def missing_branch_arcs(self): + """Return arcs that weren't executed from branch lines. + + Returns {l1:[l2a,l2b,...], ...} + + """ + missing = self.arcs_missing() + branch_lines = set(self.branch_lines()) + mba = {} + for l1, l2 in missing: + if l1 in branch_lines: + if l1 not in mba: + mba[l1] = [] + mba[l1].append(l2) + return mba + + def branch_stats(self): + """Get stats about branches. + + Returns a dict mapping line numbers to a tuple: + (total_exits, taken_exits). + """ + + exit_counts = self.parser.exit_counts() + missing_arcs = self.missing_branch_arcs() + stats = {} + for lnum in self.branch_lines(): + exits = exit_counts[lnum] + try: + missing = len(missing_arcs[lnum]) + except KeyError: + missing = 0 + stats[lnum] = (exits, exits - missing) + return stats + + +class Numbers(object): + """The numerical results of measuring coverage. + + This holds the basic statistics from `Analysis`, and is used to roll + up statistics across files. + + """ + # A global to determine the precision on coverage percentages, the number + # of decimal places. + _precision = 0 + _near0 = 1.0 # These will change when _precision is changed. + _near100 = 99.0 + + def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0, + n_branches=0, n_partial_branches=0, n_missing_branches=0 + ): + self.n_files = n_files + self.n_statements = n_statements + self.n_excluded = n_excluded + self.n_missing = n_missing + self.n_branches = n_branches + self.n_partial_branches = n_partial_branches + self.n_missing_branches = n_missing_branches + + def set_precision(cls, precision): + """Set the number of decimal places used to report percentages.""" + assert 0 <= precision < 10 + cls._precision = precision + cls._near0 = 1.0 / 10**precision + cls._near100 = 100.0 - cls._near0 + set_precision = classmethod(set_precision) + + def _get_n_executed(self): + """Returns the number of executed statements.""" + return self.n_statements - self.n_missing + n_executed = property(_get_n_executed) + + def _get_n_executed_branches(self): + """Returns the number of executed branches.""" + return self.n_branches - self.n_missing_branches + n_executed_branches = property(_get_n_executed_branches) + + def _get_pc_covered(self): + """Returns a single percentage value for coverage.""" + if self.n_statements > 0: + pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) / + (self.n_statements + self.n_branches)) + else: + pc_cov = 100.0 + return pc_cov + pc_covered = property(_get_pc_covered) + + def _get_pc_covered_str(self): + """Returns the percent covered, as a string, without a percent sign. + + Note that "0" is only returned when the value is truly zero, and "100" + is only returned when the value is truly 100. Rounding can never + result in either "0" or "100". + + """ + pc = self.pc_covered + if 0 < pc < self._near0: + pc = self._near0 + elif self._near100 < pc < 100: + pc = self._near100 + else: + pc = round(pc, self._precision) + return "%.*f" % (self._precision, pc) + pc_covered_str = property(_get_pc_covered_str) + + def pc_str_width(cls): + """How many characters wide can pc_covered_str be?""" + width = 3 # "100" + if cls._precision > 0: + width += 1 + cls._precision + return width + pc_str_width = classmethod(pc_str_width) + + def __add__(self, other): + nums = Numbers() + nums.n_files = self.n_files + other.n_files + nums.n_statements = self.n_statements + other.n_statements + nums.n_excluded = self.n_excluded + other.n_excluded + nums.n_missing = self.n_missing + other.n_missing + nums.n_branches = self.n_branches + other.n_branches + nums.n_partial_branches = ( + self.n_partial_branches + other.n_partial_branches + ) + nums.n_missing_branches = ( + self.n_missing_branches + other.n_missing_branches + ) + return nums + + def __radd__(self, other): + # Implementing 0+Numbers allows us to sum() a list of Numbers. + if other == 0: + return self + return NotImplemented diff --git a/coverage/summary.py b/coverage/summary.py new file mode 100644 index 0000000..c99c530 --- /dev/null +++ b/coverage/summary.py @@ -0,0 +1,86 @@ +"""Summary reporting""" + +import sys + +from coverage.report import Reporter +from coverage.results import Numbers +from coverage.misc import NotPython + + +class SummaryReporter(Reporter): + """A reporter for writing the summary report.""" + + def __init__(self, coverage, config): + super(SummaryReporter, self).__init__(coverage, config) + self.branches = coverage.data.has_arcs() + + def report(self, morfs, outfile=None): + """Writes a report summarizing coverage statistics per module. + + `outfile` is a file object to write the summary to. + + """ + self.find_code_units(morfs) + + # Prepare the formatting strings + max_name = max([len(cu.name) for cu in self.code_units] + [5]) + fmt_name = "%%- %ds " % max_name + fmt_err = "%s %s: %s\n" + header = (fmt_name % "Name") + " Stmts Miss" + fmt_coverage = fmt_name + "%6d %6d" + if self.branches: + header += " Branch BrMiss" + fmt_coverage += " %6d %6d" + width100 = Numbers.pc_str_width() + header += "%*s" % (width100+4, "Cover") + fmt_coverage += "%%%ds%%%%" % (width100+3,) + if self.config.show_missing: + header += " Missing" + fmt_coverage += " %s" + rule = "-" * len(header) + "\n" + header += "\n" + fmt_coverage += "\n" + + if not outfile: + outfile = sys.stdout + + # Write the header + outfile.write(header) + outfile.write(rule) + + total = Numbers() + + for cu in self.code_units: + try: + analysis = self.coverage._analyze(cu) + nums = analysis.numbers + args = (cu.name, nums.n_statements, nums.n_missing) + if self.branches: + args += (nums.n_branches, nums.n_missing_branches) + args += (nums.pc_covered_str,) + if self.config.show_missing: + args += (analysis.missing_formatted(),) + outfile.write(fmt_coverage % args) + total += nums + except KeyboardInterrupt: # pragma: not covered + raise + except: + report_it = not self.config.ignore_errors + if report_it: + typ, msg = sys.exc_info()[:2] + if typ is NotPython and not cu.should_be_python(): + report_it = False + if report_it: + outfile.write(fmt_err % (cu.name, typ.__name__, msg)) + + if total.n_files > 1: + outfile.write(rule) + args = ("TOTAL", total.n_statements, total.n_missing) + if self.branches: + args += (total.n_branches, total.n_missing_branches) + args += (total.pc_covered_str,) + if self.config.show_missing: + args += ("",) + outfile.write(fmt_coverage % args) + + return total.pc_covered diff --git a/coverage/templite.py b/coverage/templite.py new file mode 100644 index 0000000..c39e061 --- /dev/null +++ b/coverage/templite.py @@ -0,0 +1,166 @@ +"""A simple Python template renderer, for a nano-subset of Django syntax.""" + +# Coincidentally named the same as http://code.activestate.com/recipes/496702/ + +import re, sys + +class Templite(object): + """A simple template renderer, for a nano-subset of Django syntax. + + Supported constructs are extended variable access:: + + {{var.modifer.modifier|filter|filter}} + + loops:: + + {% for var in list %}...{% endfor %} + + and ifs:: + + {% if var %}...{% endif %} + + Comments are within curly-hash markers:: + + {# This will be ignored #} + + Construct a Templite with the template text, then use `render` against a + dictionary context to create a finished string. + + """ + def __init__(self, text, *contexts): + """Construct a Templite with the given `text`. + + `contexts` are dictionaries of values to use for future renderings. + These are good for filters and global values. + + """ + self.text = text + self.context = {} + for context in contexts: + self.context.update(context) + + # Split the text to form a list of tokens. + toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) + + # Parse the tokens into a nested list of operations. Each item in the + # list is a tuple with an opcode, and arguments. They'll be + # interpreted by TempliteEngine. + # + # When parsing an action tag with nested content (if, for), the current + # ops list is pushed onto ops_stack, and the parsing continues in a new + # ops list that is part of the arguments to the if or for op. + ops = [] + ops_stack = [] + for tok in toks: + if tok.startswith('{{'): + # Expression: ('exp', expr) + ops.append(('exp', tok[2:-2].strip())) + elif tok.startswith('{#'): + # Comment: ignore it and move on. + continue + elif tok.startswith('{%'): + # Action tag: split into words and parse further. + words = tok[2:-2].strip().split() + if words[0] == 'if': + # If: ('if', (expr, body_ops)) + if_ops = [] + assert len(words) == 2 + ops.append(('if', (words[1], if_ops))) + ops_stack.append(ops) + ops = if_ops + elif words[0] == 'for': + # For: ('for', (varname, listexpr, body_ops)) + assert len(words) == 4 and words[2] == 'in' + for_ops = [] + ops.append(('for', (words[1], words[3], for_ops))) + ops_stack.append(ops) + ops = for_ops + elif words[0].startswith('end'): + # Endsomething. Pop the ops stack + ops = ops_stack.pop() + assert ops[-1][0] == words[0][3:] + else: + raise SyntaxError("Don't understand tag %r" % words) + else: + ops.append(('lit', tok)) + + assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0] + self.ops = ops + + def render(self, context=None): + """Render this template by applying it to `context`. + + `context` is a dictionary of values to use in this rendering. + + """ + # Make the complete context we'll use. + ctx = dict(self.context) + if context: + ctx.update(context) + + # Run it through an engine, and return the result. + engine = _TempliteEngine(ctx) + engine.execute(self.ops) + return "".join(engine.result) + + +class _TempliteEngine(object): + """Executes Templite objects to produce strings.""" + def __init__(self, context): + self.context = context + self.result = [] + + def execute(self, ops): + """Execute `ops` in the engine. + + Called recursively for the bodies of if's and loops. + + """ + for op, args in ops: + if op == 'lit': + self.result.append(args) + elif op == 'exp': + try: + self.result.append(str(self.evaluate(args))) + except: + exc_class, exc, _ = sys.exc_info() + new_exc = exc_class("Couldn't evaluate {{ %s }}: %s" + % (args, exc)) + raise new_exc + elif op == 'if': + expr, body = args + if self.evaluate(expr): + self.execute(body) + elif op == 'for': + var, lis, body = args + vals = self.evaluate(lis) + for val in vals: + self.context[var] = val + self.execute(body) + else: + raise AssertionError("TempliteEngine doesn't grok op %r" % op) + + def evaluate(self, expr): + """Evaluate an expression. + + `expr` can have pipes and dots to indicate data access and filtering. + + """ + if "|" in expr: + pipes = expr.split("|") + value = self.evaluate(pipes[0]) + for func in pipes[1:]: + value = self.evaluate(func)(value) + elif "." in expr: + dots = expr.split('.') + value = self.evaluate(dots[0]) + for dot in dots[1:]: + try: + value = getattr(value, dot) + except AttributeError: + value = value[dot] + if hasattr(value, '__call__'): + value = value() + else: + value = self.context[expr] + return value diff --git a/coverage/tracer.c b/coverage/tracer.c new file mode 100644 index 0000000..97dd113 --- /dev/null +++ b/coverage/tracer.c @@ -0,0 +1,730 @@ +/* C-based Tracer for Coverage. */ + +#include "Python.h" +#include "compile.h" /* in 2.3, this wasn't part of Python.h */ +#include "eval.h" /* or this. */ +#include "structmember.h" +#include "frameobject.h" + +/* Compile-time debugging helpers */ +#undef WHAT_LOG /* Define to log the WHAT params in the trace function. */ +#undef TRACE_LOG /* Define to log our bookkeeping. */ +#undef COLLECT_STATS /* Collect counters: stats are printed when tracer is stopped. */ + +#if COLLECT_STATS +#define STATS(x) x +#else +#define STATS(x) +#endif + +/* Py 2.x and 3.x compatibility */ + +#ifndef Py_TYPE +#define Py_TYPE(o) (((PyObject*)(o))->ob_type) +#endif + +#if PY_MAJOR_VERSION >= 3 + +#define MyText_Type PyUnicode_Type +#define MyText_Check(o) PyUnicode_Check(o) +#define MyText_AS_BYTES(o) PyUnicode_AsASCIIString(o) +#define MyText_AS_STRING(o) PyBytes_AS_STRING(o) +#define MyInt_FromLong(l) PyLong_FromLong(l) + +#define MyType_HEAD_INIT PyVarObject_HEAD_INIT(NULL, 0) + +#else + +#define MyText_Type PyString_Type +#define MyText_Check(o) PyString_Check(o) +#define MyText_AS_BYTES(o) (Py_INCREF(o), o) +#define MyText_AS_STRING(o) PyString_AS_STRING(o) +#define MyInt_FromLong(l) PyInt_FromLong(l) + +#define MyType_HEAD_INIT PyObject_HEAD_INIT(NULL) 0, + +#endif /* Py3k */ + +/* The values returned to indicate ok or error. */ +#define RET_OK 0 +#define RET_ERROR -1 + +/* An entry on the data stack. For each call frame, we need to record the + dictionary to capture data, and the last line number executed in that + frame. +*/ +typedef struct { + PyObject * file_data; /* PyMem_Malloc'ed, a borrowed ref. */ + int last_line; +} DataStackEntry; + +/* The CTracer type. */ + +typedef struct { + PyObject_HEAD + + /* Python objects manipulated directly by the Collector class. */ + PyObject * should_trace; + PyObject * warn; + PyObject * data; + PyObject * should_trace_cache; + PyObject * arcs; + + /* Has the tracer been started? */ + int started; + /* Are we tracing arcs, or just lines? */ + int tracing_arcs; + + /* + The data stack is a stack of dictionaries. Each dictionary collects + data for a single source file. The data stack parallels the call stack: + each call pushes the new frame's file data onto the data stack, and each + return pops file data off. + + The file data is a dictionary whose form depends on the tracing options. + If tracing arcs, the keys are line number pairs. If not tracing arcs, + the keys are line numbers. In both cases, the value is irrelevant + (None). + */ + /* The index of the last-used entry in data_stack. */ + int depth; + /* The file data at each level, or NULL if not recording. */ + DataStackEntry * data_stack; + int data_stack_alloc; /* number of entries allocated at data_stack. */ + + /* The current file_data dictionary. Borrowed. */ + PyObject * cur_file_data; + + /* The line number of the last line recorded, for tracing arcs. + -1 means there was no previous line, as when entering a code object. + */ + int last_line; + + /* The parent frame for the last exception event, to fix missing returns. */ + PyFrameObject * last_exc_back; + int last_exc_firstlineno; + +#if COLLECT_STATS + struct { + unsigned int calls; + unsigned int lines; + unsigned int returns; + unsigned int exceptions; + unsigned int others; + unsigned int new_files; + unsigned int missed_returns; + unsigned int stack_reallocs; + unsigned int errors; + } stats; +#endif /* COLLECT_STATS */ +} CTracer; + +#define STACK_DELTA 100 + +static int +CTracer_init(CTracer *self, PyObject *args_unused, PyObject *kwds_unused) +{ +#if COLLECT_STATS + self->stats.calls = 0; + self->stats.lines = 0; + self->stats.returns = 0; + self->stats.exceptions = 0; + self->stats.others = 0; + self->stats.new_files = 0; + self->stats.missed_returns = 0; + self->stats.stack_reallocs = 0; + self->stats.errors = 0; +#endif /* COLLECT_STATS */ + + self->should_trace = NULL; + self->warn = NULL; + self->data = NULL; + self->should_trace_cache = NULL; + self->arcs = NULL; + + self->started = 0; + self->tracing_arcs = 0; + + self->depth = -1; + self->data_stack = PyMem_Malloc(STACK_DELTA*sizeof(DataStackEntry)); + if (self->data_stack == NULL) { + STATS( self->stats.errors++; ) + PyErr_NoMemory(); + return RET_ERROR; + } + self->data_stack_alloc = STACK_DELTA; + + self->cur_file_data = NULL; + self->last_line = -1; + + self->last_exc_back = NULL; + + return RET_OK; +} + +static void +CTracer_dealloc(CTracer *self) +{ + if (self->started) { + PyEval_SetTrace(NULL, NULL); + } + + Py_XDECREF(self->should_trace); + Py_XDECREF(self->warn); + Py_XDECREF(self->data); + Py_XDECREF(self->should_trace_cache); + + PyMem_Free(self->data_stack); + + Py_TYPE(self)->tp_free((PyObject*)self); +} + +#if TRACE_LOG +static const char * +indent(int n) +{ + static const char * spaces = + " " + " " + " " + " " + ; + return spaces + strlen(spaces) - n*2; +} + +static int logging = 0; +/* Set these constants to be a file substring and line number to start logging. */ +static const char * start_file = "tests/views"; +static int start_line = 27; + +static void +showlog(int depth, int lineno, PyObject * filename, const char * msg) +{ + if (logging) { + printf("%s%3d ", indent(depth), depth); + if (lineno) { + printf("%4d", lineno); + } + else { + printf(" "); + } + if (filename) { + PyObject *ascii = MyText_AS_BYTES(filename); + printf(" %s", MyText_AS_STRING(ascii)); + Py_DECREF(ascii); + } + if (msg) { + printf(" %s", msg); + } + printf("\n"); + } +} + +#define SHOWLOG(a,b,c,d) showlog(a,b,c,d) +#else +#define SHOWLOG(a,b,c,d) +#endif /* TRACE_LOG */ + +#if WHAT_LOG +static const char * what_sym[] = {"CALL", "EXC ", "LINE", "RET "}; +#endif + +/* Record a pair of integers in self->cur_file_data. */ +static int +CTracer_record_pair(CTracer *self, int l1, int l2) +{ + int ret = RET_OK; + + PyObject * t = Py_BuildValue("(ii)", l1, l2); + if (t != NULL) { + if (PyDict_SetItem(self->cur_file_data, t, Py_None) < 0) { + STATS( self->stats.errors++; ) + ret = RET_ERROR; + } + Py_DECREF(t); + } + else { + STATS( self->stats.errors++; ) + ret = RET_ERROR; + } + return ret; +} + +/* + * The Trace Function + */ +static int +CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unused) +{ + int ret = RET_OK; + PyObject * filename = NULL; + PyObject * tracename = NULL; + #if WHAT_LOG || TRACE_LOG + PyObject * ascii = NULL; + #endif + + #if WHAT_LOG + if (what <= sizeof(what_sym)/sizeof(const char *)) { + ascii = MyText_AS_BYTES(frame->f_code->co_filename); + printf("trace: %s @ %s %d\n", what_sym[what], MyText_AS_STRING(ascii), frame->f_lineno); + Py_DECREF(ascii); + } + #endif + + #if TRACE_LOG + ascii = MyText_AS_BYTES(frame->f_code->co_filename); + if (strstr(MyText_AS_STRING(ascii), start_file) && frame->f_lineno == start_line) { + logging = 1; + } + Py_DECREF(ascii); + #endif + + /* See below for details on missing-return detection. */ + if (self->last_exc_back) { + if (frame == self->last_exc_back) { + /* Looks like someone forgot to send a return event. We'll clear + the exception state and do the RETURN code here. Notice that the + frame we have in hand here is not the correct frame for the RETURN, + that frame is gone. Our handling for RETURN doesn't need the + actual frame, but we do log it, so that will look a little off if + you're looking at the detailed log. + + If someday we need to examine the frame when doing RETURN, then + we'll need to keep more of the missed frame's state. + */ + STATS( self->stats.missed_returns++; ) + if (self->depth >= 0) { + if (self->tracing_arcs && self->cur_file_data) { + if (CTracer_record_pair(self, self->last_line, -self->last_exc_firstlineno) < 0) { + return RET_ERROR; + } + } + SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn"); + self->cur_file_data = self->data_stack[self->depth].file_data; + self->last_line = self->data_stack[self->depth].last_line; + self->depth--; + } + } + self->last_exc_back = NULL; + } + + + switch (what) { + case PyTrace_CALL: /* 0 */ + STATS( self->stats.calls++; ) + /* Grow the stack. */ + self->depth++; + if (self->depth >= self->data_stack_alloc) { + STATS( self->stats.stack_reallocs++; ) + /* We've outgrown our data_stack array: make it bigger. */ + int bigger = self->data_stack_alloc + STACK_DELTA; + DataStackEntry * bigger_data_stack = PyMem_Realloc(self->data_stack, bigger * sizeof(DataStackEntry)); + if (bigger_data_stack == NULL) { + STATS( self->stats.errors++; ) + PyErr_NoMemory(); + self->depth--; + return RET_ERROR; + } + self->data_stack = bigger_data_stack; + self->data_stack_alloc = bigger; + } + + /* Push the current state on the stack. */ + self->data_stack[self->depth].file_data = self->cur_file_data; + self->data_stack[self->depth].last_line = self->last_line; + + /* Check if we should trace this line. */ + filename = frame->f_code->co_filename; + tracename = PyDict_GetItem(self->should_trace_cache, filename); + if (tracename == NULL) { + STATS( self->stats.new_files++; ) + /* We've never considered this file before. */ + /* Ask should_trace about it. */ + PyObject * args = Py_BuildValue("(OO)", filename, frame); + tracename = PyObject_Call(self->should_trace, args, NULL); + Py_DECREF(args); + if (tracename == NULL) { + /* An error occurred inside should_trace. */ + STATS( self->stats.errors++; ) + return RET_ERROR; + } + if (PyDict_SetItem(self->should_trace_cache, filename, tracename) < 0) { + STATS( self->stats.errors++; ) + return RET_ERROR; + } + } + else { + Py_INCREF(tracename); + } + + /* If tracename is a string, then we're supposed to trace. */ + if (MyText_Check(tracename)) { + PyObject * file_data = PyDict_GetItem(self->data, tracename); + if (file_data == NULL) { + file_data = PyDict_New(); + if (file_data == NULL) { + STATS( self->stats.errors++; ) + return RET_ERROR; + } + ret = PyDict_SetItem(self->data, tracename, file_data); + Py_DECREF(file_data); + if (ret < 0) { + STATS( self->stats.errors++; ) + return RET_ERROR; + } + } + self->cur_file_data = file_data; + /* Make the frame right in case settrace(gettrace()) happens. */ + Py_INCREF(self); + frame->f_trace = (PyObject*)self; + SHOWLOG(self->depth, frame->f_lineno, filename, "traced"); + } + else { + self->cur_file_data = NULL; + SHOWLOG(self->depth, frame->f_lineno, filename, "skipped"); + } + + Py_DECREF(tracename); + + self->last_line = -1; + break; + + case PyTrace_RETURN: /* 3 */ + STATS( self->stats.returns++; ) + /* A near-copy of this code is above in the missing-return handler. */ + if (self->depth >= 0) { + if (self->tracing_arcs && self->cur_file_data) { + int first = frame->f_code->co_firstlineno; + if (CTracer_record_pair(self, self->last_line, -first) < 0) { + return RET_ERROR; + } + } + + SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "return"); + self->cur_file_data = self->data_stack[self->depth].file_data; + self->last_line = self->data_stack[self->depth].last_line; + self->depth--; + } + break; + + case PyTrace_LINE: /* 2 */ + STATS( self->stats.lines++; ) + if (self->depth >= 0) { + SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "line"); + if (self->cur_file_data) { + /* We're tracing in this frame: record something. */ + if (self->tracing_arcs) { + /* Tracing arcs: key is (last_line,this_line). */ + if (CTracer_record_pair(self, self->last_line, frame->f_lineno) < 0) { + return RET_ERROR; + } + } + else { + /* Tracing lines: key is simply this_line. */ + PyObject * this_line = MyInt_FromLong(frame->f_lineno); + if (this_line == NULL) { + STATS( self->stats.errors++; ) + return RET_ERROR; + } + ret = PyDict_SetItem(self->cur_file_data, this_line, Py_None); + Py_DECREF(this_line); + if (ret < 0) { + STATS( self->stats.errors++; ) + return RET_ERROR; + } + } + } + self->last_line = frame->f_lineno; + } + break; + + case PyTrace_EXCEPTION: + /* Some code (Python 2.3, and pyexpat anywhere) fires an exception event + without a return event. To detect that, we'll keep a copy of the + parent frame for an exception event. If the next event is in that + frame, then we must have returned without a return event. We can + synthesize the missing event then. + + Python itself fixed this problem in 2.4. Pyexpat still has the bug. + I've reported the problem with pyexpat as http://bugs.python.org/issue6359 . + If it gets fixed, this code should still work properly. Maybe some day + the bug will be fixed everywhere coverage.py is supported, and we can + remove this missing-return detection. + + More about this fix: http://nedbatchelder.com/blog/200907/a_nasty_little_bug.html + */ + STATS( self->stats.exceptions++; ) + self->last_exc_back = frame->f_back; + self->last_exc_firstlineno = frame->f_code->co_firstlineno; + break; + + default: + STATS( self->stats.others++; ) + break; + } + + return RET_OK; +} + +/* + * Python has two ways to set the trace function: sys.settrace(fn), which + * takes a Python callable, and PyEval_SetTrace(func, obj), which takes + * a C function and a Python object. The way these work together is that + * sys.settrace(pyfn) calls PyEval_SetTrace(builtin_func, pyfn), using the + * Python callable as the object in PyEval_SetTrace. So sys.gettrace() + * simply returns the Python object used as the second argument to + * PyEval_SetTrace. So sys.gettrace() will return our self parameter, which + * means it must be callable to be used in sys.settrace(). + * + * So we make our self callable, equivalent to invoking our trace function. + * + * To help with the process of replaying stored frames, this function has an + * optional keyword argument: + * + * def CTracer_call(frame, event, arg, lineno=0) + * + * If provided, the lineno argument is used as the line number, and the + * frame's f_lineno member is ignored. + */ +static PyObject * +CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) +{ + PyFrameObject *frame; + PyObject *what_str; + PyObject *arg; + int lineno = 0; + int what; + int orig_lineno; + PyObject *ret = NULL; + + static char *what_names[] = { + "call", "exception", "line", "return", + "c_call", "c_exception", "c_return", + NULL + }; + + #if WHAT_LOG + printf("pytrace\n"); + #endif + + static char *kwlist[] = {"frame", "event", "arg", "lineno", NULL}; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!O|i:Tracer_call", kwlist, + &PyFrame_Type, &frame, &MyText_Type, &what_str, &arg, &lineno)) { + goto done; + } + + /* In Python, the what argument is a string, we need to find an int + for the C function. */ + for (what = 0; what_names[what]; what++) { + PyObject *ascii = MyText_AS_BYTES(what_str); + int should_break = !strcmp(MyText_AS_STRING(ascii), what_names[what]); + Py_DECREF(ascii); + if (should_break) { + break; + } + } + + /* Save off the frame's lineno, and use the forced one, if provided. */ + orig_lineno = frame->f_lineno; + if (lineno > 0) { + frame->f_lineno = lineno; + } + + /* Invoke the C function, and return ourselves. */ + if (CTracer_trace(self, frame, what, arg) == RET_OK) { + Py_INCREF(self); + ret = (PyObject *)self; + } + + /* Clean up. */ + frame->f_lineno = orig_lineno; + +done: + return ret; +} + +static PyObject * +CTracer_start(CTracer *self, PyObject *args_unused) +{ + PyEval_SetTrace((Py_tracefunc)CTracer_trace, (PyObject*)self); + self->started = 1; + self->tracing_arcs = self->arcs && PyObject_IsTrue(self->arcs); + self->last_line = -1; + + /* start() returns a trace function usable with sys.settrace() */ + Py_INCREF(self); + return (PyObject *)self; +} + +static PyObject * +CTracer_stop(CTracer *self, PyObject *args_unused) +{ + if (self->started) { + PyEval_SetTrace(NULL, NULL); + self->started = 0; + } + + return Py_BuildValue(""); +} + +static PyObject * +CTracer_get_stats(CTracer *self) +{ +#if COLLECT_STATS + return Py_BuildValue( + "{sI,sI,sI,sI,sI,sI,sI,sI,si,sI}", + "calls", self->stats.calls, + "lines", self->stats.lines, + "returns", self->stats.returns, + "exceptions", self->stats.exceptions, + "others", self->stats.others, + "new_files", self->stats.new_files, + "missed_returns", self->stats.missed_returns, + "stack_reallocs", self->stats.stack_reallocs, + "stack_alloc", self->data_stack_alloc, + "errors", self->stats.errors + ); +#else + return Py_BuildValue(""); +#endif /* COLLECT_STATS */ +} + +static PyMemberDef +CTracer_members[] = { + { "should_trace", T_OBJECT, offsetof(CTracer, should_trace), 0, + PyDoc_STR("Function indicating whether to trace a file.") }, + + { "warn", T_OBJECT, offsetof(CTracer, warn), 0, + PyDoc_STR("Function for issuing warnings.") }, + + { "data", T_OBJECT, offsetof(CTracer, data), 0, + PyDoc_STR("The raw dictionary of trace data.") }, + + { "should_trace_cache", T_OBJECT, offsetof(CTracer, should_trace_cache), 0, + PyDoc_STR("Dictionary caching should_trace results.") }, + + { "arcs", T_OBJECT, offsetof(CTracer, arcs), 0, + PyDoc_STR("Should we trace arcs, or just lines?") }, + + { NULL } +}; + +static PyMethodDef +CTracer_methods[] = { + { "start", (PyCFunction) CTracer_start, METH_VARARGS, + PyDoc_STR("Start the tracer") }, + + { "stop", (PyCFunction) CTracer_stop, METH_VARARGS, + PyDoc_STR("Stop the tracer") }, + + { "get_stats", (PyCFunction) CTracer_get_stats, METH_VARARGS, + PyDoc_STR("Get statistics about the tracing") }, + + { NULL } +}; + +static PyTypeObject +CTracerType = { + MyType_HEAD_INIT + "coverage.CTracer", /*tp_name*/ + sizeof(CTracer), /*tp_basicsize*/ + 0, /*tp_itemsize*/ + (destructor)CTracer_dealloc, /*tp_dealloc*/ + 0, /*tp_print*/ + 0, /*tp_getattr*/ + 0, /*tp_setattr*/ + 0, /*tp_compare*/ + 0, /*tp_repr*/ + 0, /*tp_as_number*/ + 0, /*tp_as_sequence*/ + 0, /*tp_as_mapping*/ + 0, /*tp_hash */ + (ternaryfunc)CTracer_call, /*tp_call*/ + 0, /*tp_str*/ + 0, /*tp_getattro*/ + 0, /*tp_setattro*/ + 0, /*tp_as_buffer*/ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ + "CTracer objects", /* tp_doc */ + 0, /* tp_traverse */ + 0, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + CTracer_methods, /* tp_methods */ + CTracer_members, /* tp_members */ + 0, /* tp_getset */ + 0, /* tp_base */ + 0, /* tp_dict */ + 0, /* tp_descr_get */ + 0, /* tp_descr_set */ + 0, /* tp_dictoffset */ + (initproc)CTracer_init, /* tp_init */ + 0, /* tp_alloc */ + 0, /* tp_new */ +}; + +/* Module definition */ + +#define MODULE_DOC PyDoc_STR("Fast coverage tracer.") + +#if PY_MAJOR_VERSION >= 3 + +static PyModuleDef +moduledef = { + PyModuleDef_HEAD_INIT, + "coverage.tracer", + MODULE_DOC, + -1, + NULL, /* methods */ + NULL, + NULL, /* traverse */ + NULL, /* clear */ + NULL +}; + + +PyObject * +PyInit_tracer(void) +{ + PyObject * mod = PyModule_Create(&moduledef); + if (mod == NULL) { + return NULL; + } + + CTracerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&CTracerType) < 0) { + Py_DECREF(mod); + return NULL; + } + + Py_INCREF(&CTracerType); + PyModule_AddObject(mod, "CTracer", (PyObject *)&CTracerType); + + return mod; +} + +#else + +void +inittracer(void) +{ + PyObject * mod; + + mod = Py_InitModule3("coverage.tracer", NULL, MODULE_DOC); + if (mod == NULL) { + return; + } + + CTracerType.tp_new = PyType_GenericNew; + if (PyType_Ready(&CTracerType) < 0) { + return; + } + + Py_INCREF(&CTracerType); + PyModule_AddObject(mod, "CTracer", (PyObject *)&CTracerType); +} + +#endif /* Py3k */ diff --git a/coverage/version.py b/coverage/version.py new file mode 100644 index 0000000..181ba84 --- /dev/null +++ b/coverage/version.py @@ -0,0 +1,9 @@ +"""The version and URL for coverage.py""" +# This file is exec'ed in setup.py, don't import anything! + +__version__ = "3.6" # see detailed history in CHANGES.txt + +__url__ = "http://nedbatchelder.com/code/coverage" +if max(__version__).isalpha(): + # For pre-releases, use a version-specific URL. + __url__ += "/" + __version__ diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py new file mode 100644 index 0000000..301bc86 --- /dev/null +++ b/coverage/xmlreport.py @@ -0,0 +1,150 @@ +"""XML reporting for coverage.py""" + +import os, sys, time +import xml.dom.minidom + +from coverage import __url__, __version__ +from coverage.backward import sorted, rpartition # pylint: disable=W0622 +from coverage.report import Reporter + +def rate(hit, num): + """Return the fraction of `hit`/`num`, as a string.""" + return "%.4g" % (float(hit) / (num or 1.0)) + + +class XmlReporter(Reporter): + """A reporter for writing Cobertura-style XML coverage results.""" + + def __init__(self, coverage, config): + super(XmlReporter, self).__init__(coverage, config) + + self.packages = None + self.xml_out = None + self.arcs = coverage.data.has_arcs() + + def report(self, morfs, outfile=None): + """Generate a Cobertura-compatible XML report for `morfs`. + + `morfs` is a list of modules or filenames. + + `outfile` is a file object to write the XML to. + + """ + # Initial setup. + outfile = outfile or sys.stdout + + # Create the DOM that will store the data. + impl = xml.dom.minidom.getDOMImplementation() + docType = impl.createDocumentType( + "coverage", None, + "http://cobertura.sourceforge.net/xml/coverage-03.dtd" + ) + self.xml_out = impl.createDocument(None, "coverage", docType) + + # Write header stuff. + xcoverage = self.xml_out.documentElement + xcoverage.setAttribute("version", __version__) + xcoverage.setAttribute("timestamp", str(int(time.time()*1000))) + xcoverage.appendChild(self.xml_out.createComment( + " Generated by coverage.py: %s " % __url__ + )) + xpackages = self.xml_out.createElement("packages") + xcoverage.appendChild(xpackages) + + # Call xml_file for each file in the data. + self.packages = {} + self.report_files(self.xml_file, morfs) + + lnum_tot, lhits_tot = 0, 0 + bnum_tot, bhits_tot = 0, 0 + + # Populate the XML DOM with the package info. + for pkg_name in sorted(self.packages.keys()): + pkg_data = self.packages[pkg_name] + class_elts, lhits, lnum, bhits, bnum = pkg_data + xpackage = self.xml_out.createElement("package") + xpackages.appendChild(xpackage) + xclasses = self.xml_out.createElement("classes") + xpackage.appendChild(xclasses) + for class_name in sorted(class_elts.keys()): + xclasses.appendChild(class_elts[class_name]) + xpackage.setAttribute("name", pkg_name.replace(os.sep, '.')) + xpackage.setAttribute("line-rate", rate(lhits, lnum)) + xpackage.setAttribute("branch-rate", rate(bhits, bnum)) + xpackage.setAttribute("complexity", "0") + + lnum_tot += lnum + lhits_tot += lhits + bnum_tot += bnum + bhits_tot += bhits + + xcoverage.setAttribute("line-rate", rate(lhits_tot, lnum_tot)) + xcoverage.setAttribute("branch-rate", rate(bhits_tot, bnum_tot)) + + # Use the DOM to write the output file. + outfile.write(self.xml_out.toprettyxml()) + + # Return the total percentage. + return 100.0 * (lhits_tot + bhits_tot) / (lnum_tot + bnum_tot) + + def xml_file(self, cu, analysis): + """Add to the XML report for a single file.""" + + # Create the 'lines' and 'package' XML elements, which + # are populated later. Note that a package == a directory. + package_name = rpartition(cu.name, ".")[0] + className = cu.name + + package = self.packages.setdefault(package_name, [{}, 0, 0, 0, 0]) + + xclass = self.xml_out.createElement("class") + + xclass.appendChild(self.xml_out.createElement("methods")) + + xlines = self.xml_out.createElement("lines") + xclass.appendChild(xlines) + + xclass.setAttribute("name", className) + filename = cu.file_locator.relative_filename(cu.filename) + xclass.setAttribute("filename", filename.replace("\\", "/")) + xclass.setAttribute("complexity", "0") + + branch_stats = analysis.branch_stats() + + # For each statement, create an XML 'line' element. + for line in analysis.statements: + xline = self.xml_out.createElement("line") + xline.setAttribute("number", str(line)) + + # Q: can we get info about the number of times a statement is + # executed? If so, that should be recorded here. + xline.setAttribute("hits", str(int(line not in analysis.missing))) + + if self.arcs: + if line in branch_stats: + total, taken = branch_stats[line] + xline.setAttribute("branch", "true") + xline.setAttribute("condition-coverage", + "%d%% (%d/%d)" % (100*taken/total, taken, total) + ) + xlines.appendChild(xline) + + class_lines = len(analysis.statements) + class_hits = class_lines - len(analysis.missing) + + if self.arcs: + class_branches = sum([t for t,k in branch_stats.values()]) + missing_branches = sum([t-k for t,k in branch_stats.values()]) + class_br_hits = class_branches - missing_branches + else: + class_branches = 0.0 + class_br_hits = 0.0 + + # Finalize the statistics that are collected in the XML DOM. + xclass.setAttribute("line-rate", rate(class_hits, class_lines)) + xclass.setAttribute("branch-rate", rate(class_br_hits, class_branches)) + package[0][className] = xclass + package[1] += class_hits + package[2] += class_lines + package[3] += class_br_hits + package[4] += class_branches diff --git a/doc/api.rst b/doc/api.rst new file mode 100644 index 0000000..fe59bc2 --- /dev/null +++ b/doc/api.rst @@ -0,0 +1,47 @@ +.. _api: + +============ +Coverage API +============ + +:history: 20090524T134300, brand new docs. +:history: 20090613T164000, final touches for 3.0 +:history: 20100221T151500, docs for 3.3 (on the plane back from PyCon) +:history: 20100725T211700, updated for 3.4. +:history: 20121111T235800, added a bit of clarification. + + +The API to coverage.py is very simple, contained in a single module called +`coverage`. Most of the interface is in a single class, also called +`coverage`. Methods on the coverage object correspond roughly to operations available +in the command line interface. For example, a simple use would be:: + + import coverage + + cov = coverage.coverage() + cov.start() + + # .. call your code .. + + cov.stop() + cov.save() + + cov.html_report() + + +The coverage module +------------------- + +.. module:: coverage + +.. autoclass:: coverage + :members: + + +Starting coverage automatically +------------------------------- + +This function is used to start coverage measurement automatically when Python +starts. See :ref:`subprocess` for details. + +.. autofunction:: process_startup diff --git a/doc/branch.rst b/doc/branch.rst new file mode 100644 index 0000000..13b9dc6 --- /dev/null +++ b/doc/branch.rst @@ -0,0 +1,120 @@ +.. _branch: + +=========================== +Branch coverage measurement +=========================== + +:history: 20091127T201300, new for version 3.2 +:history: 20100725T211700, updated for 3.4. +:history: 20110604T181700, updated for 3.5. +:history: 20111214T181800, Fix a bug that Guido pointed out. + +.. highlight:: python + :linenothreshold: 5 + + +In addition to the usual statement coverage, Coverage.py also supports branch +coverage measurement. Where a line in your program could jump to more than one +next line, coverage.py tracks which of those destinations are actually visited, +and flags lines that haven't visited all of their possible destinations. + +For example:: + + def my_partial_fn(x): # line 1 + if x: # 2 + y = 10 # 3 + return y # 4 + + my_partial_fn(1) + +In this code, line 2 is an ``if`` statement which can go next to either line 3 +or line 4. Statement coverage would show all lines of the function as executed. +But the if was never evaluated as false, so line 2 never jumps to line 4. + +Branch coverage will flag this code as not fully covered because of the missing +jump from line 2 to line 4. This is known as a partial branch. + + +How to measure branch coverage +------------------------------ + +To measure branch coverage, run coverage.py with the ``--branch`` flag:: + + coverage run --branch myprog.py + +When you report on the results with ``coverage report`` or ``coverage html``, +the percentage of branch possibilities taken will be included in the percentage +covered total for each file. The coverage percentage for a file is the actual +executions divided by the execution opportunities. Each line in the file is an +execution opportunity, as is each branch destination. + +The HTML report gives information about which lines had missing branches. Lines +that were missing some branches are shown in yellow, with an annotation at the +far right showing branch destination line numbers that were not exercised. + +The XML report produced by ``coverage xml`` also includes branch information, +including separate statement and branch coverage percentages. + + +How it works +------------ + +When measuring branches, coverage.py collects pairs of line numbers, a source +and destination for each transition from one line to another. Static analysis +of the compiled bytecode provides a list of possible transitions. Comparing +the measured to the possible indicates missing branches. + +The idea of tracking how lines follow each other was from `Titus Brown`__. +Thanks, Titus! + +__ http://ivory.idyll.org/blog + + +Excluding code +-------------- + +If you have :ref:`excluded code `, a condtional will not be +counted as a branch if one of its choices is excluded:: + + def only_one_choice(x): + if x: + blah1() + blah2() + else: # pragma: no cover + # x is always true. + blah3() + +Because the ``else`` clause is excluded, the ``if`` only has one possible +next line, so it isn't considered a branch at all. + + +Structurally partial branches +----------------------------- + +Sometimes branching constructs are used in unusual ways that don't actually +branch. For example:: + + while True: + if cond: + break + do_something() + +Here the while loop will never exit normally, so it doesn't take both of its +"possible" branches. For some of these constructs, such as "while True:" and +"if 0:", coverage.py understands what is going on. In these cases, the line +will not be marked as a partial branch. + +But there are many ways in your own code to write intentionally partial +branches, and you don't want coverage.py pestering you about them. You can +tell coverage.py that you don't want them flagged by marking them with a +pragma:: + + i = 0 + while i < 999999999: # pragma: no branch + if eventually(): + break + +Here the while loop will never complete because the break will always be taken +at some point. Coverage.py can't work that out on its own, but the +"no branch" pragma indicates that the branch is known to be partial, and +the line is not flagged. diff --git a/doc/changes.rst b/doc/changes.rst new file mode 100644 index 0000000..576c996 --- /dev/null +++ b/doc/changes.rst @@ -0,0 +1,629 @@ +.. _changes: + +==================================== +Major change history for coverage.py +==================================== + +:history: 20090524T134300, brand new docs. +:history: 20090613T164000, final touches for 3.0 +:history: 20090706T205000, changes for 3.0.1 +:history: 20091004T170700, changes for 3.1 +:history: 20091128T072200, changes for 3.2 +:history: 20091205T161525, 3.2 final +:history: 20100221T151900, changes for 3.3 +:history: 20100306T181400, changes for 3.3.1 +:history: 20100725T211700, updated for 3.4. +:history: 20100820T151500, updated for 3.4b1 +:history: 20100906T133800, updated for 3.4b2 +:history: 20100919T163400, updated for 3.4 release. +:history: 20110604T214100, updated for 3.5b1 +:history: 20110629T082200, updated for 3.5 +:history: 20110923T081600, updated for 3.5.1 +:history: 20120429T162100, updated for 3.5.2b1 +:history: 20120503T233700, updated for 3.5.2 +:history: 20120929T093100, updated for 3.5.3 +:history: 20121129T060100, updated for 3.6b1. +:history: 20121223T180600, updated for 3.6b2. +:history: 20130105T173500, updated for 3.6 + + +These are the major changes for coverage.py. For a more complete change +history, see the `CHANGES.txt`_ file in the source tree. + +.. _CHANGES.txt: http://bitbucket.org/ned/coveragepy/src/tip/CHANGES.txt + + +Version 3.6 --- 5 January 2013 +------------------------------ + +Features: + +- The **report**, **html**, and **xml** commands now accept a ``--fail-under`` + switch that indicates in the exit status whether the coverage percentage was + less than a particular value. Closes `issue 139`_. + +- The reporting functions coverage.report(), coverage.html_report(), and + coverage.xml_report() now all return a float, the total percentage covered + measurement. + +- The HTML report's title can now be set in the configuration file, with the + ``--title`` switch on the command line, or via the API. + +- Configuration files now support substitution of environment variables, using + syntax like ``${WORD}``. Closes `issue 97`_. + +Packaging: + +- The C extension is optionally compiled using a different more widely-used + technique, taking another stab at fixing `issue 80`_ once and for all. + +- When installing, now in addition to creating a "coverage" command, two new + aliases are also installed. A "coverage2" or "coverage3" command will be + created, depending on whether you are installing in Python 2.x or 3.x. + A "coverage-X.Y" command will also be created corresponding to your specific + version of Python. Closes `issue 111`_. + +- The coverage.py installer no longer tries to bootstrap setuptools or + Distribute. You must have one of them installed first, as `issue 202`_ + recommended. + +- The coverage.py kit now includes docs (closing `issue 137`_) and tests. + +Docs: + +- Added a page to the docs about :doc:`contributing ` to + coverage.py, closing `issue 171`_. + +- Added a page to the docs about :doc:`troublesome situations `, + closing `issue 226`_. + +- Docstrings for the legacy singleton methods are more helpful. Thanks Marius + Gedminas. Closes `issue 205`_. + +- The pydoc tool can now show docmentation for the class `coverage.coverage`. + Closes `issue 206`_. + +- Added some info to the TODO file, closing `issue 227`_. + +Fixes: + +- Wildcards in ``include=`` and ``omit=`` arguments were not handled properly + in reporting functions, though they were when running. Now they are handled + uniformly, closing `issue 143`_ and `issue 163`_. **NOTE**: it is possible + that your configurations may now be incorrect. If you use ``include`` or + ``omit`` during reporting, whether on the command line, through the API, or + in a configuration file, please check carefully that you were not relying on + the old broken behavior. + +- Embarrassingly, the `[xml] output=` setting in the .coveragerc file simply + didn't work. Now it does. + +- Combining data files would create entries for phantom files if used with + ``source`` and path aliases. It no longer does. + +- ``debug sys`` now shows the configuration file path that was read. + +- If an oddly-behaved package claims that code came from an empty-string + filename, coverage.py no longer associates it with the directory name, + fixing `issue 221`_. + +- The XML report now consistently uses filenames for the filename attribute, + rather than sometimes using module names. Fixes `issue 67`_. + Thanks, Marcus Cobden. + +- Coverage percentage metrics are now computed slightly differently under + branch coverage. This means that completely unexecuted files will now + correctly have 0% coverage, fixing `issue 156`_. This also means that your + total coverage numbers will generally now be lower if you are measuring + branch coverage. + +- On Windows, files are now reported in their correct case, fixing `issue 89`_ + and `issue 203`_. + +- If a file is missing during reporting, the path shown in the error message + is now correct, rather than an incorrect path in the current directory. + Fixes `issue 60`_. + +- Running an HTML report in Python 3 in the same directory as an old Python 2 + HTML report would fail with a UnicodeDecodeError. This issue (`issue 193`_) + is now fixed. + +- Fixed yet another error trying to parse non-Python files as Python, this + time an IndentationError, closing `issue 82`_ for the fourth time... + +- If `coverage xml` fails because there is no data to report, it used to + create a zero-length XML file. Now it doesn't, fixing `issue 210`_. + +- Jython files now work with the ``--source`` option, fixing `issue 100`_. + +- Running coverage under a debugger is unlikely to work, but it shouldn't fail + with "TypeError: 'NoneType' object is not iterable". Fixes `issue 201`_. + +- On some Linux distributions, when installed with the OS package manager, + coverage.py would report its own code as part of the results. Now it won't, + fixing `issue 214`_, though this will take some time to be repackaged by the + operating systems. + +- When coverage.py ended unsuccessfully, it may have reported odd errors like + ``'NoneType' object has no attribute 'isabs'``. It no longer does, + so kiss `issue 153`_ goodbye. + + +.. _issue 60: https://bitbucket.org/ned/coveragepy/issue/60/incorrect-path-to-orphaned-pyc-files +.. _issue 67: https://bitbucket.org/ned/coveragepy/issue/67/xml-report-filenames-may-be-generated +.. _issue 80: https://bitbucket.org/ned/coveragepy/issue/80/is-there-a-duck-typing-way-to-know-we-cant +.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _issue 89: https://bitbucket.org/ned/coveragepy/issue/89/on-windows-all-packages-are-reported-in +.. _issue 97: https://bitbucket.org/ned/coveragepy/issue/97/allow-environment-variables-to-be +.. _issue 100: https://bitbucket.org/ned/coveragepy/issue/100/source-directive-doesnt-work-for-packages +.. _issue 111: https://bitbucket.org/ned/coveragepy/issue/111/when-installing-coverage-with-pip-not +.. _issue 137: https://bitbucket.org/ned/coveragepy/issue/137/provide-docs-with-source-distribution +.. _issue 139: https://bitbucket.org/ned/coveragepy/issue/139/easy-check-for-a-certain-coverage-in-tests +.. _issue 143: https://bitbucket.org/ned/coveragepy/issue/143/omit-doesnt-seem-to-work-in-coverage +.. _issue 153: https://bitbucket.org/ned/coveragepy/issue/153/non-existent-filename-triggers +.. _issue 156: https://bitbucket.org/ned/coveragepy/issue/156/a-completely-unexecuted-file-shows-14 +.. _issue 163: https://bitbucket.org/ned/coveragepy/issue/163/problem-with-include-and-omit-filename +.. _issue 171: https://bitbucket.org/ned/coveragepy/issue/171/how-to-contribute-and-run-tests +.. _issue 193: https://bitbucket.org/ned/coveragepy/issue/193/unicodedecodeerror-on-htmlpy +.. _issue 201: https://bitbucket.org/ned/coveragepy/issue/201/coverage-using-django-14-with-pydb-on +.. _issue 202: https://bitbucket.org/ned/coveragepy/issue/202/get-rid-of-ez_setuppy-and +.. _issue 203: https://bitbucket.org/ned/coveragepy/issue/203/duplicate-filenames-reported-when-filename +.. _issue 205: https://bitbucket.org/ned/coveragepy/issue/205/make-pydoc-coverage-more-friendly +.. _issue 206: https://bitbucket.org/ned/coveragepy/issue/206/pydoc-coveragecoverage-fails-with-an-error +.. _issue 210: https://bitbucket.org/ned/coveragepy/issue/210/if-theres-no-coverage-data-coverage-xml +.. _issue 214: https://bitbucket.org/ned/coveragepy/issue/214/coveragepy-measures-itself-on-precise +.. _issue 221: https://bitbucket.org/ned/coveragepy/issue/221/coveragepy-incompatible-with-pyratemp +.. _issue 226: https://bitbucket.org/ned/coveragepy/issue/226/make-readme-section-to-describe-when +.. _issue 227: https://bitbucket.org/ned/coveragepy/issue/227/update-todo + + +Version 3.5.3 --- 29 September 2012 +----------------------------------- + +- Line numbers in the HTML report line up better with the source lines, fixing + `issue 197`_, thanks Marius Gedminas. + +- When specifying a directory as the source= option, the directory itself no + longer needs to have a ``__init__.py`` file, though its subdirectories do, to + be considered as source files. + +- Files encoded as UTF-8 with a BOM are now properly handled, fixing + `issue 179`_. Thanks, Pablo Carballo. + +- Fixed more cases of non-Python files being reported as Python source, and + then not being able to parse them as Python. Closes `issue 82`_ (again). + Thanks, Julian Berman. + +- Fixed memory leaks under Python 3, thanks, Brett Cannon. Closes `issue 147`_. + +- Optimized .pyo files may not have been handled correctly, `issue 195`_. + Thanks, Marius Gedminas. + +- Certain unusually named file paths could have been mangled during reporting, + `issue 194`_. Thanks, Marius Gedminas. + +- Try to do a better job of the impossible task of detecting when we can't + build the C extension, fixing `issue 183`_. + +.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _issue 147: https://bitbucket.org/ned/coveragepy/issue/147/massive-memory-usage-by-ctracer +.. _issue 179: https://bitbucket.org/ned/coveragepy/issue/179/htmlreporter-fails-when-source-file-is +.. _issue 183: https://bitbucket.org/ned/coveragepy/issue/183/install-fails-for-python-23 +.. _issue 194: https://bitbucket.org/ned/coveragepy/issue/194/filelocatorrelative_filename-could-mangle +.. _issue 195: https://bitbucket.org/ned/coveragepy/issue/195/pyo-file-handling-in-codeunit +.. _issue 197: https://bitbucket.org/ned/coveragepy/issue/197/line-numbers-in-html-report-do-not-align + + +Version 3.5.2 --- 4 May 2012 +---------------------------- + +- The HTML report has slightly tweaked controls: the buttons at the top of + the page are color-coded to the source lines they affect. + +- Custom CSS can be applied to the HTML report by specifying a CSS file as + the extra_css configuration value in the [html] section. + +- Source files with custom encodings declared in a comment at the top are now + properly handled during reporting on Python 2. Python 3 always handled them + properly. This fixes `issue 157`_. + +- Backup files left behind by editors are no longer collected by the source= + option, fixing `issue 168`_. + +- If a file doesn't parse properly as Python, we don't report it as an error + if the filename seems like maybe it wasn't meant to be Python. This is a + pragmatic fix for `issue 82`_. + +- The ``-m`` switch on ``coverage report``, which includes missing line numbers + in the summary report, can now be specifed as ``show_missing`` in the + config file. Closes `issue 173`_. + +- When running a module with ``coverage run -m ``, certain details + of the execution environment weren't the same as for + ``python -m ``. This had the unfortunate side-effect of making + ``coverage run -m unittest discover`` not work if you had tests in a + directory named "test". This fixes `issue 155`_. + +- Now the exit status of your product code is properly used as the process + status when running ``python -m coverage run ...``. Thanks, JT Olds. + +- When installing into pypy, we no longer attempt (and fail) to compile + the C tracer function, closing `issue 166`_. + +.. _issue 82: https://bitbucket.org/ned/coveragepy/issue/82/tokenerror-when-generating-html-report +.. _issue 155: https://bitbucket.org/ned/coveragepy/issue/155/cant-use-coverage-run-m-unittest-discover +.. _issue 157: https://bitbucket.org/ned/coveragepy/issue/157/chokes-on-source-files-with-non-utf-8 +.. _issue 166: https://bitbucket.org/ned/coveragepy/issue/166/dont-try-to-compile-c-extension-on-pypy +.. _issue 168: https://bitbucket.org/ned/coveragepy/issue/168/dont-be-alarmed-by-emacs-droppings +.. _issue 173: https://bitbucket.org/ned/coveragepy/issue/173/theres-no-way-to-specify-show-missing-in + + +Version 3.5.1 --- 23 September 2011 +----------------------------------- + +- When combining data files from parallel runs, you can now instruct coverage + about which directories are equivalent on different machines. A ``[paths]`` + section in the configuration file lists paths that are to be considered + equivalent. Finishes `issue 17`_. + +- for-else constructs are understood better, and don't cause erroneous partial + branch warnings. Fixes `issue 122`_. + +- Branch coverage for ``with`` statements is improved, fixing `issue 128`_. + +- The number of partial branches reported on the HTML summary page was + different than the number reported on the individual file pages. This is + now fixed. + +- An explicit include directive to measure files in the Python installation + wouldn't work because of the standard library exclusion. Now the include + directive takes precendence, and the files will be measured. Fixes + `issue 138`_. + +- The HTML report now handles Unicode characters in Python source files + properly. This fixes `issue 124`_ and `issue 144`_. Thanks, Devin + Jeanpierre. + +- In order to help the core developers measure the test coverage of the + standard library, Brandon Rhodes devised an aggressive hack to trick Python + into running some coverage code before anything else in the process. + See the coverage/fullcoverage directory if you are interested. + +.. _issue 17: http://bitbucket.org/ned/coveragepy/issue/17/support-combining-coverage-data-from +.. _issue 122: http://bitbucket.org/ned/coveragepy/issue/122/for-else-always-reports-missing-branch +.. _issue 124: http://bitbucket.org/ned/coveragepy/issue/124/no-arbitrary-unicode-in-html-reports-in +.. _issue 128: http://bitbucket.org/ned/coveragepy/issue/128/branch-coverage-of-with-statement-in-27 +.. _issue 138: http://bitbucket.org/ned/coveragepy/issue/138/include-should-take-precedence-over-is +.. _issue 144: http://bitbucket.org/ned/coveragepy/issue/144/failure-generating-html-output-for + + +Version 3.5 --- 29 June 2011 +---------------------------- + +HTML reporting: + +- The HTML report now has hotkeys. Try ``n``, ``s``, ``m``, ``x``, ``b``, + ``p``, and ``c`` on the overview page to change the column sorting. + On a file page, ``r``, ``m``, ``x``, and ``p`` toggle the run, missing, + excluded, and partial line markings. You can navigate the highlighted + sections of code by using the ``j`` and ``k`` keys for next and previous. + The ``1`` (one) key jumps to the first highlighted section in the file, + and ``0`` (zero) scrolls to the top of the file. + +- HTML reporting is now incremental: a record is kept of the data that + produced the HTML reports, and only files whose data has changed will + be generated. This should make most HTML reporting faster. + + +Running Python files + +- Modules can now be run directly using ``coverage run -m modulename``, to + mirror Python's ``-m`` flag. Closes `issue 95`_, thanks, Brandon Rhodes. + +- ``coverage run`` didn't emulate Python accurately in one detail: the + current directory inserted into ``sys.path`` was relative rather than + absolute. This is now fixed. + +- Pathological code execution could disable the trace function behind our + backs, leading to incorrect code measurement. Now if this happens, + coverage.py will issue a warning, at least alerting you to the problem. + Closes `issue 93`_. Thanks to Marius Gedminas for the idea. + +- The C-based trace function now behaves properly when saved and restored + with ``sys.gettrace()`` and ``sys.settrace()``. This fixes `issue 125`_ + and `issue 123`_. Thanks, Devin Jeanpierre. + +- Coverage.py can now be run directly from a working tree by specifying + the directory name to python: ``python coverage_py_working_dir run ...``. + Thanks, Brett Cannon. + +- A little bit of Jython support: `coverage run` can now measure Jython + execution by adapting when $py.class files are traced. Thanks, Adi Roiban. + + +Reporting + +- Partial branch warnings can now be pragma'd away. The configuration option + ``partial_branches`` is a list of regular expressions. Lines matching any of + those expressions will never be marked as a partial branch. In addition, + there's a built-in list of regular expressions marking statements which should + never be marked as partial. This list includes ``while True:``, ``while 1:``, + ``if 1:``, and ``if 0:``. + +- The ``--omit`` and ``--include`` switches now interpret their values more + usefully. If the value starts with a wildcard character, it is used as-is. + If it does not, it is interpreted relative to the current directory. + Closes `issue 121`_. + +- Syntax errors in supposed Python files can now be ignored during reporting + with the ``-i`` switch just like other source errors. Closes `issue 115`_. + +.. _issue 93: http://bitbucket.org/ned/coveragepy/issue/93/copying-a-mock-object-breaks-coverage +.. _issue 95: https://bitbucket.org/ned/coveragepy/issue/95/run-subcommand-should-take-a-module-name +.. _issue 115: https://bitbucket.org/ned/coveragepy/issue/115/fail-gracefully-when-reporting-on-file +.. _issue 121: https://bitbucket.org/ned/coveragepy/issue/121/filename-patterns-are-applied-stupidly +.. _issue 123: https://bitbucket.org/ned/coveragepy/issue/123/pyeval_settrace-used-in-way-that-breaks +.. _issue 125: https://bitbucket.org/ned/coveragepy/issue/125/coverage-removes-decoratortoolss-tracing + + +Version 3.4 --- 19 September 2010 +--------------------------------- + +Controlling source: + +- BACKWARD INCOMPATIBILITY: the ``--omit`` and ``--include`` switches now take + file patterns rather than file prefixes, closing `issue 34`_ and `issue 36`_. + +- BACKWARD INCOMPATIBILITY: the `omit_prefixes` argument is gone throughout + coverage.py, replaced with `omit`, a list of filename patterns suitable for + `fnmatch`. A parallel argument `include` controls what files are included. + +- The run command now has a ``--source`` switch, a list of directories or + module names. If provided, coverage.py will only measure execution in those + source files. The run command also now supports ``--include`` and ``--omit`` + to control what modules it measures. This can speed execution and reduce the + amount of data during reporting. Thanks Zooko. + +- The reporting commands (report, annotate, html, and xml) now have an + ``--include`` switch to restrict reporting to modules matching those file + patterns, similar to the existing ``--omit`` switch. Thanks, Zooko. + +Reporting: + +- Completely unexecuted files can now be included in coverage results, reported + as 0% covered. This only happens if the --source option is specified, since + coverage.py needs guidance about where to look for source files. + +- Python files with no statements, for example, empty ``__init__.py`` files, + are now reported as having zero statements instead of one. Fixes `issue 1`_. + +- Reports now have a column of missed line counts rather than executed line + counts, since developers should focus on reducing the missed lines to zero, + rather than increasing the executed lines to varying targets. Once + suggested, this seemed blindingly obvious. + +- Coverage percentages are now displayed uniformly across reporting methods. + Previously, different reports could round percentages differently. Also, + percentages are only reported as 0% or 100% if they are truly 0 or 100, and + are rounded otherwise. Fixes `issue 41`_ and `issue 70`_. + +- The XML report output now properly includes a percentage for branch coverage, + fixing `issue 65`_ and `issue 81`_, and the report is sorted by package + name, fixing `issue 88`_. + +- The XML report is now sorted by package name, fixing `issue 88`_. + +- The precision of reported coverage percentages can be set with the + ``[report] precision`` config file setting. Completes `issue 16`_. + +- Line numbers in HTML source pages are clickable, linking directly to that + line, which is highlighted on arrival. Added a link back to the index page + at the bottom of each HTML page. + +Execution and measurement: + +- Various warnings are printed to stderr for problems encountered during data + measurement: if a ``--source`` module has no Python source to measure, or is + never encountered at all, or if no data is collected. + +- Doctest text files are no longer recorded in the coverage data, since they + can't be reported anyway. Fixes `issue 52`_ and `issue 61`_. + +- Threads derived from ``threading.Thread`` with an overridden `run` method + would report no coverage for the `run` method. This is now fixed, closing + `issue 85`_. + +- Programs that exited with ``sys.exit()`` with no argument weren't handled + properly, producing a coverage.py stack trace. This is now fixed. + +- Programs that call ``os.fork`` will properly collect data from both the child + and parent processes. Use ``coverage run -p`` to get two data files that can + be combined with ``coverage combine``. Fixes `issue 56`_. + +- When measuring code running in a virtualenv, most of the system library was + being measured when it shouldn't have been. This is now fixed. + +- Coverage can now be run as a module: ``python -m coverage``. Thanks, + Brett Cannon. + +.. _issue 1: http://bitbucket.org/ned/coveragepy/issue/1/empty-__init__py-files-are-reported-as-1-executable +.. _issue 16: http://bitbucket.org/ned/coveragepy/issue/16/allow-configuration-of-accuracy-of-percentage-totals +.. _issue 34: http://bitbucket.org/ned/coveragepy/issue/34/enhanced-omit-globbing-handling +.. _issue 36: http://bitbucket.org/ned/coveragepy/issue/36/provide-regex-style-omit +.. _issue 41: http://bitbucket.org/ned/coveragepy/issue/41/report-says-100-when-it-isnt-quite-there +.. _issue 52: http://bitbucket.org/ned/coveragepy/issue/52/doctesttestfile-confuses-source-detection +.. _issue 56: http://bitbucket.org/ned/coveragepy/issue/56/coveragepy-cant-trace-child-processes-of-a +.. _issue 61: http://bitbucket.org/ned/coveragepy/issue/61/annotate-i-doesnt-work +.. _issue 65: http://bitbucket.org/ned/coveragepy/issue/65/branch-option-not-reported-in-cobertura +.. _issue 70: http://bitbucket.org/ned/coveragepy/issue/70/text-report-and-html-report-disagree-on-coverage +.. _issue 81: http://bitbucket.org/ned/coveragepy/issue/81/xml-report-does-not-have-condition-coverage-attribute-for-lines-with-a +.. _issue 85: http://bitbucket.org/ned/coveragepy/issue/85/threadrun-isnt-measured +.. _issue 88: http://bitbucket.org/ned/coveragepy/issue/88/xml-report-lists-packages-in-random-order + + +Version 3.3.1 --- 6 March 2010 +------------------------------ + +- Using ``parallel=True`` in a .coveragerc file prevented reporting, but now + does not, fixing `issue 49`_. + +- When running your code with ``coverage run``, if you call ``sys.exit()``, + coverage.py will exit with that status code, fixing `issue 50`_. + +.. _issue 49: http://bitbucket.org/ned/coveragepy/issue/49 +.. _issue 50: http://bitbucket.org/ned/coveragepy/issue/50 + + +Version 3.3 --- 24 February 2010 +-------------------------------- + +- Settings are now read from a .coveragerc file. A specific file can be + specified on the command line with ``--rcfile=FILE``. The name of the file + can be programmatically set with the ``config_file`` argument to the + coverage() constructor, or reading a config file can be disabled with + ``config_file=False``. + +- Added coverage.process_start to enable coverage measurement when Python + starts. + +- Parallel data file names now have a random number appended to them in + addition to the machine name and process id. Also, parallel data files + combined with ``coverage combine`` are deleted after they're combined, to + clean up unneeded files. Fixes `issue 40`_. + +- Exceptions thrown from product code run with ``coverage run`` are now + displayed without internal coverage.py frames, so the output is the same as + when the code is run without coverage.py. + +- Fixed `issue 39`_ and `issue 47`_. + +.. _issue 39: http://bitbucket.org/ned/coveragepy/issue/39 +.. _issue 40: http://bitbucket.org/ned/coveragepy/issue/40 +.. _issue 47: http://bitbucket.org/ned/coveragepy/issue/47 + + +Version 3.2 --- 5 December 2009 +------------------------------- + +- Branch coverage: coverage.py can tell you which branches didn't have both (or + all) choices executed, even where the choice doesn't affect which lines were + executed. See :ref:`branch` for more details. + +- The table of contents in the HTML report is now sortable: click the headers + on any column. The sorting is persisted so that subsequent reports are + sorted as you wish. Thanks, `Chris Adams`_. + +- XML reporting has file paths that let Cobertura find the source code, fixing + `issue 21`_. + +- The ``--omit`` option now works much better than before, fixing `issue 14`_ + and `issue 33`_. Thanks, Danek Duvall. + +- Added a ``--version`` option on the command line. + +- Program execution under coverage is a few percent faster. + +- Some exceptions reported by the command line interface have been cleaned up + so that tracebacks inside coverage.py aren't shown. Fixes `issue 23`_. + +- Fixed some problems syntax coloring sources with line continuations and + source with tabs: `issue 30`_ and `issue 31`_. + +.. _Chris Adams: http://improbable.org/chris/ +.. _issue 21: http://bitbucket.org/ned/coveragepy/issue/21 +.. _issue 23: http://bitbucket.org/ned/coveragepy/issue/23 +.. _issue 14: http://bitbucket.org/ned/coveragepy/issue/14 +.. _issue 30: http://bitbucket.org/ned/coveragepy/issue/30 +.. _issue 31: http://bitbucket.org/ned/coveragepy/issue/31 +.. _issue 33: http://bitbucket.org/ned/coveragepy/issue/33 + + +Version 3.1 --- 4 October 2009 +------------------------------ + +- Python 3.1 is now supported. + +- Coverage.py has a new command line syntax with sub-commands. This expands + the possibilities for adding features and options in the future. The old + syntax is still supported. Try ``coverage help`` to see the new commands. + Thanks to Ben Finney for early help. + +- Added an experimental ``coverage xml`` command for producing coverage reports + in a Cobertura-compatible XML format. Thanks, Bill Hart. + +- Added the ``--timid`` option to enable a simpler slower trace function that + works for DecoratorTools projects, including TurboGears. Fixed `issue 12`_ + and `issue 13`_. + +- HTML reports now display syntax-colored Python source. + +- Added a ``coverage debug`` command for getting diagnostic information about + the coverage.py installation. + +- Source code can now be read from eggs. Thanks, `Ross Lawley`_. Fixes + `issue 25`_. + +.. _Ross Lawley: http://agileweb.org/ +.. _issue 25: http://bitbucket.org/ned/coveragepy/issue/25 +.. _issue 12: http://bitbucket.org/ned/coveragepy/issue/12 +.. _issue 13: http://bitbucket.org/ned/coveragepy/issue/13 + + +Version 3.0.1 --- 7 July 2009 +----------------------------- + +- Removed the recursion limit in the tracer function. Previously, code that + ran more than 500 frames deep would crash. + +- Fixed a bizarre problem involving pyexpat, whereby lines following XML parser + invocations could be overlooked. + +- On Python 2.3, coverage.py could mis-measure code with exceptions being + raised. This is now fixed. + +- The coverage.py code itself will now not be measured by coverage.py, and no + coverage modules will be mentioned in the nose ``--with-cover`` plugin. + +- When running source files, coverage.py now opens them in universal newline + mode just like Python does. This lets it run Windows files on Mac, for + example. + + +Version 3.0 --- 13 June 2009 +---------------------------- + +- Coverage is now a package rather than a module. Functionality has been split + into classes. + +- HTML reports and annotation of source files: use the new ``-b`` (browser) + switch. Thanks to George Song for code, inspiration and guidance. + +- The trace function is implemented in C for speed. Coverage runs are now + much faster. Thanks to David Christian for productive micro-sprints and + other encouragement. + +- The minimum supported Python version is 2.3. + +- When using the object api (that is, constructing a coverage() object), data + is no longer saved automatically on process exit. You can re-enable it with + the ``auto_data=True`` parameter on the coverage() constructor. + The module-level interface still uses automatic saving. + +- Code in the Python standard library is not measured by default. If you need + to measure standard library code, use the ``-L`` command-line switch during + execution, or the ``cover_pylib=True`` argument to the coverage() + constructor. + +- API changes: + + - Added parameters to coverage.__init__ for options that had been set on + the coverage object itself. + + - Added clear_exclude() and get_exclude_list() methods for programmatic + manipulation of the exclude regexes. + + - Added coverage.load() to read previously-saved data from the data file. + + - coverage.annotate_file is no longer available. + + - Removed the undocumented cache_file argument to coverage.usecache(). diff --git a/doc/cmd.rst b/doc/cmd.rst new file mode 100644 index 0000000..6b1d15d --- /dev/null +++ b/doc/cmd.rst @@ -0,0 +1,365 @@ +.. _cmd: + +=========================== +Coverage command line usage +=========================== + +:history: 20090524T134300, brand new docs. +:history: 20090613T164000, final touches for 3.0 +:history: 20090913T084400, new command line syntax +:history: 20091004T170700, changes for 3.1 +:history: 20091127T200700, changes for 3.2 +:history: 20100223T200600, changes for 3.3 +:history: 20100725T211700, updated for 3.4 +:history: 20110827T212500, updated for 3.5.1, combining aliases +:history: 20120119T075600, Added some clarification from George Paci +:history: 20120504T091800, Added info about execution warnings, and 3.5.2 stuff. +:history: 20120807T211600, Clarified the combine rules. +:history: 20121003T074600, Fixed an option reference, https://bitbucket.org/ned/coveragepy/issue/200/documentation-mentions-output-xml-instead +:history: 20121117T091000, Added command aliases. + +.. highlight:: console + + +When you install coverage.py, a command-line script simply called ``coverage`` +is placed in your Python scripts directory. To help with multi-version +installs, it will also create either a ``coverage2`` or ``coverage3`` alias, +and a ``coverage-X.Y`` alias, depending on the version of Python you're using. +For example, when installing on Python 2.7, you will be able to use +``coverage``, ``coverage2``, or ``coverage-2.7`` on the command line. + +Coverage has a number of commands which determine the action performed: + +* **run** -- Run a Python program and collect execution data. + +* **report** -- Report coverage results. + +* **html** -- Produce annotated HTML listings with coverage results. + +* **xml** -- Produce an XML report with coverage results. + +* **annotate** -- Annotate source files with coverage results. + +* **erase** -- Erase previously collected coverage data. + +* **combine** -- Combine together a number of data files. + +* **debug** -- Get diagnostic information. + +Help is available with the **help** command, or with the ``--help`` switch on +any other command:: + + $ coverage help + $ coverage help run + $ coverage run --help + +Version information for coverage.py can be displayed with +``coverage --version``. + +Any command can use a configuration file by specifying it with the +``--rcfile=FILE`` command-line switch. Any option you can set on the command +line can also be set in the configuration file. This can be a better way to +control coverage.py since the configuration file can be checked into source +control, and can provide options that other invocation techniques (like test +runner plugins) may not offer. See :ref:`config` for more details. + + +.. _cmd_execution: + +Execution +--------- + +You collect execution data by running your Python program with the **run** +command:: + + $ coverage run my_program.py arg1 arg2 + blah blah ..your program's output.. blah blah + +Your program runs just as if it had been invoked with the Python command line. +Arguments after your file name are passed to your program as usual in +``sys.argv``. Rather than providing a filename, you can use the ``-m`` switch +and specify an importable module name instead, just as you can with the +Python ``-m`` switch:: + + $ coverage run -m packagename.modulename arg1 arg2 + blah blah ..your program's output.. blah blah + +If you want :ref:`branch coverage ` measurement, use the ``--branch`` +flag. Otherwise only statement coverage is measured. + +You can specify the code to measure with the ``--source``, ``--include``, and +``--omit`` switches. See :ref:`Specifying source files ` for +details of their interpretation. Remember to put options for run after "run", +but before the program invocation:: + + $ coverage run --source=dir1,dir2 my_program.py arg1 arg2 + $ coverage run --source=dir1,dir2 -m packagename.modulename arg1 arg2 + +By default, coverage does not measure code installed with the Python +interpreter, for example, the standard library. If you want to measure that +code as well as your own, add the ``-L`` flag. + +If your coverage results seem to be overlooking code that you know has been +executed, try running coverage again with the ``--timid`` flag. This uses a +simpler but slower trace method. Projects that use DecoratorTools, including +TurboGears, will need to use ``--timid`` to get correct results. This option +can also be enabled by setting the environment variable COVERAGE_OPTIONS to +``--timid``. + +If you are measuring coverage in a multi-process program, or across a number of +machines, you'll want the ``--parallel-mode`` switch to keep the data separate +during measurement. See :ref:`cmd_combining` below. + +During execution, coverage.py may warn you about conditions it detects that +could affect the measurement process. The possible warnings include: + +* "Trace function changed, measurement is likely wrong: XXX" + + Coverage measurement depends on a Python setting called the trace function. + Other Python code in your product might change that function, which will + disrupt coverage.py's measurement. This warning indicate that has happened. + The XXX in the message is the new trace function value, which might provide + a clue to the cause. + +* "Module XXX has no Python source" + + You asked coverage.py to measure module XXX, but once it was imported, it + turned out not to have a corresponding .py file. Without a .py file, + coverage.py can't report on missing lines. + +* "Module XXX was never imported" + + You asked coverage.py to measure module XXX, but it was never imported by + your program. + +* "No data was collected" + + Coverage.py ran your program, but didn't measure any lines as executed. + This could be because you asked to measure only modules that never ran, + or for other reasons. + + + +.. _cmd_datafile: + +Data file +--------- + +Coverage collects execution data in a file called ".coverage". If need be, you +can set a new file name with the COVERAGE_FILE environment variable. + +By default,each run of your program starts with an empty data set. If you need +to run your program multiple times to get complete data (for example, because +you need to supply disjoint options), you can accumulate data across runs with +the ``-a`` flag on the **run** command. + +To erase the collected data, use the **erase** command:: + + $ coverage erase + + +.. _cmd_combining: + +Combining data files +-------------------- + +If you need to collect coverage data from different machines or processes, +coverage can combine multiple files into one for reporting. Use the ``-p`` flag +during execution to append distinguishing information to the .coverage data +file name. + +Once you have created a number of these files, you can copy them all to a single +directory, and use the **combine** command to combine them into one .coverage +data file:: + + $ coverage combine + +If the different machines run your code from different places in their file +systems, coverage won't know how to combine the data. You can tell coverage +how the different locations correlate with a ``[paths]`` section in your +configuration file. See :ref:`config_paths` for details. + +If you are collecting and renaming your own data files, you'll need to name +them properly for **combine** to find them. It looks for files named after +the data file (defaulting to ".coverage", overridable with COVERAGE_FILE), with +a dotted suffix. All such files in the current directory will be combined. +Here are some examples of combinable data files:: + + .coverage.machine1 + .coverage.20120807T212300 + .coverage.last_good_run.ok + + +.. _cmd_reporting: + +Reporting +--------- + +Coverage provides a few styles of reporting, with the **report**, **html**, +**annotate**, and **xml** commands. They share a number of common options. + +The command-line arguments are module or file names to report on, if you'd like +to report on a subset of the data collected. + +The ``--include`` and ``--omit`` flags specify lists of filename patterns. They +control which files to report on, and are described in more detail +in :ref:`source`. + +The ``-i`` or ``--ignore-errors`` switch tells coverage.py to ignore problems +encountered trying to find source files to report on. This can be useful if +some files are missing, or if your Python execution is tricky enough that file +names are synthesized without real source files. + +If you provide a ``--fail-under`` value, the total percentage covered will be +compared to that value. If it is less, the command will exit with a status +code of 2, indicating that the total coverage was less than your target. This +can be used as part of a pass/fail condition, for example in a continuous +integration server. This option isn't available for **annotate**. + + +.. _cmd_summary: + +Coverage summary +---------------- + +The simplest reporting is a textual summary produced with **report**:: + + $ coverage report + Name Stmts Miss Cover + --------------------------------------------- + my_program 20 4 80% + my_module 15 2 86% + my_other_module 56 6 89% + --------------------------------------------- + TOTAL 91 12 87% + +For each module executed, the report shows the count of executable statements, +the number of those statements missed, and the resulting coverage, expressed +as a percentage. + +The ``-m`` flag also shows the line numbers of missing statements:: + + $ coverage report -m + Name Stmts Miss Cover Missing + ------------------------------------------------------- + my_program 20 4 80% 33-35, 39 + my_module 15 2 86% 8, 12 + my_other_module 56 6 89% 17-23 + ------------------------------------------------------- + TOTAL 91 12 87% + +You can restrict the report to only certain files by naming them on the +command line:: + + $ coverage report -m my_program.py my_other_module.py + Name Stmts Miss Cover Missing + ------------------------------------------------------- + my_program 20 4 80% 33-35, 39 + my_other_module 56 6 89% 17-23 + ------------------------------------------------------- + TOTAL 76 10 87% + +Other common reporting options are described above in :ref:`cmd_reporting`. + + +.. _cmd_html: + +HTML annotation +--------------- + +Coverage can annotate your source code for which lines were executed +and which were not. The **html** command creates an HTML report similar to the +**report** summary, but as an HTML file. Each module name links to the source +file decorated to show the status of each line. + +Here's a `sample report`__. + +__ /code/coverage/sample_html/index.html + +Lines are highlighted green for executed, red for missing, and gray for +excluded. The counts at the top of the file are buttons to turn on and off +the highlighting. + +A number of keyboard shortcuts are available for navigating the report. +Click the keyboard icon in the upper right to see the complete list. + +The title of the report can be set with the ``title`` setting in the +``[html]`` section of the configuration file, or the ``--title`` switch on +the command line. + +If you prefer a different style for your HTML report, you can provide your +own CSS file to apply, by specifying a CSS file in the ``[html]`` section of +the configuration file. See :ref:`config_html` for details. + +The ``-d`` argument specifies an output directory, defaulting to "htmlcov":: + + $ coverage html -d coverage_html + +Other common reporting options are described above in :ref:`cmd_reporting`. + +Generating the HTML report can be time-consuming. Stored with the HTML report +is a data file that is used to speed up reporting the next time. If you +generate a new report into the same directory, coverage.py will skip +generating unchanged pages, making the process faster. + + +.. _cmd_annotation: + +Text annotation +--------------- + +The **annotate** command produces a text annotation of your source code. With a +``-d`` argument specifying an output directory, each Python file becomes a text +file in that directory. Without ``-d``, the files are written into the same +directories as the original Python files. + +Coverage status for each line of source is indicated with a character prefix:: + + > executed + ! missing (not executed) + - excluded + +For example:: + + # A simple function, never called with x==1 + + > def h(x): + """Silly function.""" + - if 0: #pragma: no cover + - pass + > if x == 1: + ! a = 1 + > else: + > a = 2 + +Other common reporting options are described above in :ref:`cmd_reporting`. + + +.. _cmd_xml: + +XML reporting +------------- + +The **xml** command writes coverage data to a "coverage.xml" file in a format +compatible with `Cobertura`_. + +.. _Cobertura: http://cobertura.sourceforge.net + +You can specify the name of the output file with the ``-o`` switch. + +Other common reporting options are described above in :ref:`cmd_reporting`. + + +.. _cmd_debug: + +Diagnostics +----------- + +The **debug** command shows internal information to help diagnose problems. +If you are reporting a bug about coverage.py, including the output of this +command can often help:: + + $ coverage debug sys > please_attach_to_bug_report.txt + +Two types of information are available: ``sys`` to show system configuration, +and ``data`` to show a summary of the collected coverage data. diff --git a/doc/config.rst b/doc/config.rst new file mode 100644 index 0000000..f6d8f1f --- /dev/null +++ b/doc/config.rst @@ -0,0 +1,198 @@ +.. _config: + +=================== +Configuration files +=================== + +:history: 20100223T201600, new for 3.3 +:history: 20100725T211700, updated for 3.4. +:history: 20100824T092900, added ``precision``. +:history: 20110604T184400, updated for 3.5. +:history: 20110827T212700, updated for 3.5.1 + + +Coverage.py options can be specified in a configuration file. This makes it +easier to re-run coverage with consistent settings, and also allows for +specification of options that are otherwise only available in the +:ref:`API `. + +Configuration files also make it easier to get coverage testing of spawned +sub-processes. See :ref:`subprocess` for more details. + +The default name for configuration files is ``.coveragerc``, in the same +directory coverage.py is being run in. Most of the settings in the +configuration file are tied to your source code and how it should be +measured, so it should be stored with your source, and checked into +source control, rather than put in your home directory. + + +Syntax +------ + +A coverage.py configuration file is in classic .ini file format: sections are +introduced by a ``[section]`` header, and contain ``name = value`` entries. +Lines beginning with ``#`` or ``;`` are ignored as comments. + +Strings don't need quotes. Multi-valued strings can be created by indenting +values on multiple lines. + +Boolean values can be specified as ``on``, ``off``, ``true``, ``false``, ``1``, +or ``0`` and are case-insensitive. + +Environment variables can be substituted in by using dollar signs: ``$WORD`` +``${WORD}`` will be replaced with the value of ``WORD`` in the environment. +A dollar sign can be inserted with ``$$``. Missing environment variables +will result in empty strings with no error. + +Many sections and values correspond roughly to commands and options in +the :ref:`command-line interface `. + +Here's a sample configuration file:: + + # .coveragerc to control coverage.py + [run] + branch = True + + [report] + # Regexes for lines to exclude from consideration + exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: + + ignore_errors = True + + [html] + directory = coverage_html_report + + +[run] +----- + +These values are generally used when running product code, though some apply +to more than one command. + +``branch`` (boolean, default False): whether to measure +:ref:`branch coverage ` in addition to statement coverage. + +``cover_pylib`` (boolean, default False): whether to measure the Python +standard library. + +``data_file`` (string, default ".coverage"): the name of the data file to use +for storing or reporting coverage. + +``include`` (multi-string): a list of filename patterns, the files to include +in measurement or reporting. See :ref:`source` for details. + +``omit`` (multi-string): a list of filename patterns, the files to leave out +of measurement or reporting. See :ref:`source` for details. + +``parallel`` (boolean, default False): append the machine name, process +id and random number to the data file name to simplify collecting data from +many processes. See :ref:`cmd_combining` for more information. + +``source`` (multi-string): a list of packages or directories, the source to +measure during execution. See :ref:`source` for details. + +``timid`` (boolean, default False): use a simpler but slower trace method. +Try this if you get seemingly impossible results. + + +.. _config_paths: + +[paths] +------- + +The entries in this section are lists of file paths that should be +considered equivalent when combining data from different machines:: + + [paths] + source = + src/ + /jenkins/build/*/src + c:\myproj\src + +The names of the entries are ignored, you may choose any name that +you like. The value is a lists of strings. When combining data +with the ``combine`` command, two file paths will be combined +if they start with paths from the same list. + +The first value must be an actual file path on the machine where +the reporting will happen, so that source code can be found. +The other values can be file patterns to match against the paths +of collected data. + +See :ref:`cmd_combining` for more information. + + +[report] +-------- + +Values common to many kinds of reporting. + +``exclude_lines`` (multi-string): a list of regular expressions. Any line of +your source code that matches one of these regexes is excluded from being +reported as missing. More details are in :ref:`excluding`. If you use this +option, you are replacing all the exclude regexes, so you'll need to also +supply the "pragma: no cover" regex if you still want to use it. + +``ignore_errors`` (boolean, default False): ignore source code that can't be +found. + +``include`` (multi-string): a list of filename patterns, the files to include +in reporting. See :ref:`source` for details. + +``omit`` (multi-string): a list of filename patterns, the files to leave out +of reporting. See :ref:`source` for details. + +``partial_branches`` (multi-string): a list of regular expressions. Any line +of code that matches one of these regexes is excused from being reported as +a partial branch. More details are in :ref:`branch`. If you use this option, +you are replacing all the partial branch regexes so you'll need to also +supply the "pragma: no branch" regex if you still want to use it. + +``precision`` (integer): the number of digits after the decimal point to +display for reported coverage percentages. The default is 0, displaying +for example "87%". A value of 2 will display percentages like "87.32%". + +``show_missing`` (boolean, default False): when running a summary report, +show missing lines. See :ref:`cmd_summary` for more information. + + +.. _config_html: + +[html] +------ + +Values particular to HTML reporting. The values in the ``[report]`` section +also apply to HTML output, where appropriate. + +``directory`` (string, default "htmlcov"): where to write the HTML report files. + +``extra_css`` (string): the path to a file of CSS to apply to the HTML report. +The file will be copied into the HTML output directory. Don't name it +"style.css". This CSS is in addition to the CSS normally used, though you can +overwrite as many of the rules as you like. + +``title`` (string, default "Coverage report"): the title to use for the report. +Note this is text, not HTML. + + +[xml] +----- + +Values particular to XML reporting. The values in the ``[report]`` section +also apply to XML output, where appropriate. + +``output`` (string, default "coverage.xml"): where to write the XML report. diff --git a/doc/contributing.rst b/doc/contributing.rst new file mode 100644 index 0000000..90b9ba1 --- /dev/null +++ b/doc/contributing.rst @@ -0,0 +1,164 @@ +.. _contributing: + +=========================== +Contributing to coverage.py +=========================== + +:history: 20121112T154100, brand new docs. + +.. highlight:: console + +I welcome contributions to coverage.py. Over the years, dozens of people have +provided patches of various sizes to add features or fix bugs. This page +should have all the information you need to make a contribution. + +One source of history or ideas are the `bug reports`_ against coverage.py. +There you can find ideas for requested features, or the remains of rejected +ideas. + +.. _bug reports: https://bitbucket.org/ned/coveragepy/issues?status=new&status=open + + +Before you begin +---------------- + +If you have an idea for coverage.py, run it by me before you begin writing +code. This way, I can get you going in the right direction, or point you to +previous work in the area. Things are not always as straightforward as they +seem, and having the benefit of lessons learned by those before you can save +you frustration. + + +Getting the code +---------------- + +The coverage.py code is hosted on a `Mercurial`_ repository at +https://bitbucket.org/ned/coveragepy. To get a working environment, follow +these steps: + +#. (Optional, but recommended) Create a virtualenv to work in, and activate + it. + +#. Clone the repo:: + + $ hg clone https://bitbucket.org/ned/coveragepy + +#. Install the requirements:: + + $ pip install -r requirements.txt + +#. Install a number of versions of Python. Coverage.py supports a wide range + of Python versions. The more you can test with, the more easily your code + can be used as-is. If you only have one version, that's OK too, but may + mean more work integrating your contribution. + + +Running the tests +----------------- + +The tests are written as standard unittest-style tests, and are run with +`tox`_:: + + $ tox + GLOB sdist-make: /home/ned/coverage/setup.py + py25 sdist-reinst: /home/ned/coverage/tox/dist/coverage-3.6b1.zip + py25 runtests: commands[0] + py25 runtests: commands[1] + py25 runtests: commands[2] + py25 runtests: commands[3] + py25 runtests: commands[4] + === Python 2.5.5 with Python tracer (/home/ned/coverage/tox/py25/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 360 tests in 10.836s + + OK + py25 runtests: commands[5] + py25 runtests: commands[6] + === Python 2.5.5 with C tracer (/home/ned/coverage/tox/py25/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 360 tests in 10.044s + + OK + py26 sdist-reinst: /home/ned/coverage/trunk/.tox/dist/coverage-3.6b1.zip + py26 runtests: commands[0] + py26 runtests: commands[1] + py26 runtests: commands[2] + py26 runtests: commands[3] + py26 runtests: commands[4] + === CPython 2.6.6 with Python tracer (/home/ned/coverage/tox/py26/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 364 tests in 12.572s + + OK + py26 runtests: commands[5] + py26 runtests: commands[6] + === CPython 2.6.6 with C tracer (/home/ned/coverage/tox/py26/bin/python) === + ...........................................................................................(etc) + ---------------------------------------------------------------------- + Ran 364 tests in 11.458s + + OK + (and so on...) + +Tox runs the complete test suite twice for each version of Python you have +installed. The first run uses the Python implementation of the trace +function, the second uses the C implementation. + +To limit tox to just a few versions of Python, use the ``-e`` switch:: + + $ tox -e py27,py33 + +To run just a few tests, you can use nose test selector syntax:: + + $ tox test.test_misc:SetupPyTest.test_metadata + +This looks in `test/test_misc.py` to find the `SetupPyTest` class, and runs the +`test_metadata` test method. + +Of course, run all the tests on every version of Python you have, before +submitting a change. + + +Lint, etc +--------- + +I try to keep the coverage.py as clean as possible. I use pylint to alert me +to possible problems:: + + $ make lint + pylint --rcfile=.pylintrc coverage setup.py test + python -m tabnanny coverage setup.py test + python igor.py check_eol + +The source is pylint-clean, even if it's because there are pragmas quieting +some warnings. Please try to keep it that way, but don't let pylint warnings +keep you from sending patches. I can clean them up. + + +Coverage testing coverage.py +---------------------------- + +Coverage.py can measure itself, but it's complicated. The process has been +packaged up to make it easier:: + + $ COVERAGE_COVERAGE=yes tox + $ python igor.py combine_html + +Then look at htmlcov/index.html. Note that due to the recursive nature of +coverage.py measuring itself, there are some parts of the code that will never +appear as covered, even though they are executed. + + +Contributing +------------ + +When you are ready to contribute a change, any way you can get it to me is +probably fine. A pull request on Bitbucket is great, but a simple diff or +patch is great too. + + +.. _Mercurial: http://mercurial.selenic.com/ +.. _tox: http://tox.testrun.org/ diff --git a/doc/excluding.rst b/doc/excluding.rst new file mode 100644 index 0000000..f1262c3 --- /dev/null +++ b/doc/excluding.rst @@ -0,0 +1,105 @@ +.. _excluding: + +============================ +Excluding code from coverage +============================ + +:history: 20090613T090500, brand new docs. +:history: 20100224T200900, updated for 3.3. +:history: 20100725T211700, updated for 3.4. +:history: 20110604T184400, updated for 3.5. + + +You may have code in your project that you know won't be executed, and you want +to tell coverage to ignore it. For example, you may have debugging-only code +that won't be executed during your unit tests. You can tell coverage to exclude +this code during reporting so that it doesn't clutter your reports with noise +about code that you don't need to hear about. + +Coverage will look for comments marking clauses for exclusion. In this code, +the "if debug" clause is excluded from reporting:: + + a = my_function1() + if debug: # pragma: no cover + msg = "blah blah" + log_message(msg, a) + b = my_function2() + +Any line with a comment of "pragma: no cover" is excluded. If that line +introduces a clause, for example, an if clause, or a function or class +definition, then the entire clause is also excluded. Here the __repr__ +function is not reported as missing:: + + class MyObject(object): + def __init__(self): + blah1() + blah2() + + def __repr__(self): # pragma: no cover + return "" + +Excluded code is executed as usual, and its execution is recorded in the +coverage data as usual. When producing reports though, coverage excludes it from +the list of missing code. + + +Branch coverage +--------------- + +When measuring :ref:`branch coverage `, a condtional will not be +counted as a branch if one of its choices is excluded:: + + def only_one_choice(x): + if x: + blah1() + blah2() + else: # pragma: no cover + # x is always true. + blah3() + +Because the ``else`` clause is excluded, the ``if`` only has one possible +next line, so it isn't considered a branch at all. + + +Advanced exclusion +------------------ + +Coverage identifies exclusions by matching lines against a list of regular +expressions. Using :ref:`configuration files ` or the coverage +:ref:`API `, you can add to that list. This is useful if you have +often-used constructs to exclude that can be matched with a regex. You can +exclude them all at once without littering your code with exclusion pragmas. + +For example, you might decide that __repr__ functions are usually only used +in debugging code, and are uninteresting to test themselves. You could exclude +all of them by adding a regex to the exclusion list:: + + [report] + exclude_lines = def __repr__ + +For example, here's a list of exclusions I've used:: + + [report] + exclude_lines = + pragma: no cover + def __repr__ + if self.debug: + if settings.DEBUG + raise AssertionError + raise NotImplementedError + if 0: + if __name__ == .__main__.: + +Note that when using the ``exclude_lines`` option in a configuration file, you +are taking control of the entire list of regexes, so you need to re-specify the +default "pragma: no cover" match if you still want it to apply. + +A similar pragma, "no branch", can be used to tailor branch coverage +measurement. See :ref:`branch` for details. + + +Excluding source files +---------------------- + +See :ref:`source` for ways to limit what files coverage.py measures or reports +on. diff --git a/doc/faq.rst b/doc/faq.rst new file mode 100644 index 0000000..6b0c4af --- /dev/null +++ b/doc/faq.rst @@ -0,0 +1,116 @@ +.. _faq: + +================== +FAQ and other help +================== + +:history: 20090613T141800, brand new docs. +:history: 20091005T073900, updated for 3.1. +:history: 20091127T201500, updated for 3.2. +:history: 20110605T175500, add the announcement mailing list. +:history: 20121231T104700, Tweak the py3 text. + + +Frequently asked questions +-------------------------- + +**Q: I use nose to run my tests, and its cover plugin doesn't let me create +HTML or XML reports. What should I do?** + +First run your tests and collect coverage data with `nose`_ and its plugin. +This will write coverage data into a .coverage file. Then run coverage.py from +the :ref:`command line ` to create the reports you need from that data. + +.. _nose: http://somethingaboutorange.com/mrl/projects/nose + + +**Q: Why do unexecutable lines show up as executed?** + +Usually this is because you've updated your code and run coverage on it +again without erasing the old data. Coverage records line numbers executed, so +the old data may have recorded a line number which has since moved, causing +coverage to claim a line has been executed which cannot be. + +If you are using the ``-x`` command line action, it doesn't erase first by +default. Switch to the ``coverage run`` command, or use the ``-e`` switch to +erase all data before starting the next run. + + +**Q: Why do the bodies of functions (or classes) show as executed, but the def +lines do not?** + +This happens because coverage is started after the functions are defined. The +definition lines are executed without coverage measurement, then coverage is +started, then the function is called. This means the body is measured, but +the definition of the function itself is not. + +To fix this, start coverage earlier. If you use the :ref:`command line ` +to run your program with coverage, then your entire program will be monitored. +If you are using the :ref:`API `, you need to call coverage.start() before +importing the modules that define your functions. + + +**Q: Does coverage.py work on Python 3.x?** + +Yes, Python 3 is fully supported. + + +**Q: Isn't coverage testing the best thing ever?** + +It's good, but `it isn't perfect`__. + +__ http://nedbatchelder.com/blog/200710/flaws_in_coverage_measurement.html + + +.. Other resources + --------------- + + There are a number of projects that help integrate coverage.py into other + systems: + + - `trialcoverage`_ is a plug-in for Twisted trial. + + .. _trialcoverage: http://pypi.python.org/pypi/trialcoverage + + - `pytest-coverage`_ + + .. _pytest-coverage: http://pypi.python.org/pypi/pytest-coverage + + - `django-coverage`_ for use with Django. + + .. _django-coverage: http://pypi.python.org/pypi/django-coverage + + +**Q: Where can I get more help with coverage.py?** + +You can discuss coverage.py or get help using it on the `Testing In Python`_ +mailing list. + +.. _Testing In Python: http://lists.idyll.org/listinfo/testing-in-python + +Bug reports are gladly accepted at the `Bitbucket issue tracker`_. + +.. _Bitbucket issue tracker: http://bitbucket.org/ned/coveragepy/issues + +Announcements of new coverage.py releases are sent to the +`coveragepy-announce`_ mailing list. + +.. _coveragepy-announce: http://groups.google.com/group/coveragepy-announce + +`I can be reached`__ in a number of ways, I'm happy to answer questions about +using coverage.py. I'm also available hourly for consultation or custom +development. + +__ http://nedbatchelder.com/site/aboutned.html + + +History +------- + +Coverage.py was originally written by `Gareth Rees`_. +Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of +`many others`_. The :ref:`change history ` has all the details. + +.. _Gareth Rees: http://garethrees.org/ +.. _Ned Batchelder: http://nedbatchelder.com +.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/AUTHORS.txt diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..ed163b8 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,171 @@ +=========== +coverage.py +=========== + +:history: 20090524T134300, brand new docs. +:history: 20090613T164000, final touches for 3.0 +:history: 20090618T195900, minor tweaks +:history: 20090707T205200, changes for 3.0.1 +:history: 20090913T084400, new command line syntax +:history: 20091004T211900, version 3.1 +:history: 20091127T155100, version 3.2 +:history: 20091205T161429, version 3.2 for real. +:history: 20100224T204700, version 3.3 +:history: 20100306T181500, version 3.3.1 +:history: 20100725T211700, updated for 3.4. +:history: 20100820T151500, updated for 3.4b1. +:history: 20100906T134700, updated for 3.4b2. +:history: 20100919T163500, updated for 3.4 release. +:history: 20110213T081200, claim true 3.2 compatibility. +:history: 20110604T114800, update for 3.5b1 +:history: 20110629T082300, update for 3.5 +:history: 20110827T221800, update for 3.5.1b1 +:history: 20110923T081800, update for 3.5.1 +:history: 20120429T162100, updated for 3.5.2b1 +:history: 20120503T233800, updated for 3.5.2 +:history: 20120929T093500, updated for 3.5.3 +:history: 20121117T094900, Change from easy_install to pip. +:history: 20121128T203700, Updated for 3.6b1. +:history: 20121223T180600, Updated for 3.6b2. +:history: 20121229T112300, Updated for 3.6b3. +:history: 20130105T174000, Updated for 3.6 + + +Coverage.py is a tool for measuring code coverage of Python programs. It +monitors your program, noting which parts of the code have been executed, then +analyzes the source to identify code that could have been executed but was not. + +Coverage measurement is typically used to gauge the effectiveness of tests. It +can show which parts of your code are being exercised by tests, and which are +not. + +.. ifconfig:: not prerelease + + The latest version is coverage.py 3.6, released 5 January 2013. + It is supported on Python versions 2.3 through 3.3, and PyPy 1.8. + +.. ifconfig:: prerelease + + The latest version is coverage.py 3.6b3, released 29 December 2012. + It is supported on Python versions 2.3 through 3.3, and PyPy 1.9. + **This is a pre-release build. The usual warnings about possible bugs apply.** + The latest stable version is coverage.py 3.5.3, `described here`_. + +.. _described here: http://nedbatchelder.com/code/coverage + + +Quick start +----------- + +Getting started is easy: + +#. Install coverage.py from the `coverage page on the Python Package Index`_, + or by using "pip install coverage". For a few more details, see + :ref:`install`. + +#. Use ``coverage run`` to run your program and gather data: + + .. code-block:: console + + $ coverage run my_program.py arg1 arg2 + blah blah ..your program's output.. blah blah + +#. Use ``coverage report`` to report on the results: + + .. code-block:: console + + $ coverage report -m + Name Stmts Miss Cover Missing + ------------------------------------------------------- + my_program 20 4 80% 33-35, 39 + my_other_module 56 6 89% 17-23 + ------------------------------------------------------- + TOTAL 76 10 87% + +#. For a nicer presentation, use ``coverage html`` to get annotated HTML + listings detailing missed lines: + + .. code-block:: console + + $ coverage html + + .. ifconfig:: not prerelease + + Then visit htmlcov/index.html in your browser, to see a + `report like this`_. + + .. ifconfig:: prerelease + + Then visit htmlcov/index.html in your browser, to see a + `report like this one`_. + +.. _coverage page on the Python Package Index: http://pypi.python.org/pypi/coverage +.. _report like this: /code/coverage/sample_html/index.html +.. _report like this one: /code/coverage/sample_html_beta/index.html + + +Using coverage.py +----------------- + +There are a few different ways to use coverage.py. The simplest is the +:ref:`command line `, which lets you run your program and see the results. +If you need more control over how your project is measured, you can use the +:ref:`API `. + +Some test runners provide coverage integration to make it easy to use coverage +while running tests. For example, `nose`_ has a `cover plug-in`_. + +You can fine-tune coverage's view of your code by directing it to ignore parts +that you know aren't interesting. See :ref:`source` and :ref:`excluding` for +details. + +.. _nose: http://somethingaboutorange.com/mrl/projects/nose +.. _cover plug-in: https://nose.readthedocs.org/en/latest/plugins/cover.html + + +.. _contact: + +Getting help +------------ + +If the :ref:`FAQ ` doesn't answer your question, you can discuss +coverage.py or get help using it on the `Testing In Python`_ mailing list. + +.. _Testing In Python: http://lists.idyll.org/listinfo/testing-in-python + +Bug reports are gladly accepted at the `Bitbucket issue tracker`_. +Bitbucket also hosts the `code repository`_. + +.. _Bitbucket issue tracker: http://bitbucket.org/ned/coveragepy/issues +.. _code repository: http://bitbucket.org/ned/coveragepy + +`I can be reached`_ in a number of ways. I'm happy to answer questions about +using coverage.py. I'm also available hourly for consultation or custom +development. + +.. _I can be reached: http://nedbatchelder.com/site/aboutned.html + + + +More information +---------------- + +.. toctree:: + :maxdepth: 1 + + install + cmd + config + source + excluding + branch + subprocess + api + contributing + trouble + faq + changes + + +.. How it works +.. .coverage file format diff --git a/doc/install.rst b/doc/install.rst new file mode 100644 index 0000000..9539093 --- /dev/null +++ b/doc/install.rst @@ -0,0 +1,80 @@ +.. _install: + +============ +Installation +============ + +:history: 20100725T225600, new for 3.4. +:history: 20100820T151500, updated for 3.4b1. +:history: 20100906T134800, updated for 3.4b2. +:history: 20110604T213400, updated for 3.5b1. +:history: 20110629T082400, updated for 3.5. +:history: 20110923T081900, updated for 3.5.1. +:history: 20120429T162500, updated for 3.5.2b1. +:history: 20120503T234000, updated for 3.5.2. +:history: 20120929T093600, updated for 3.5.3. +:history: 20121117T095000, Now setuptools is a pre-req. +:history: 20121128T203000, updated for 3.6b1. +:history: 20121223T180800, updated for 3.6b2. +:history: 20121229T112400, updated for 3.6b3. +:history: 20130105T174400, updated for 3.6. + + +.. highlight:: console +.. _coverage_pypi: http://pypi.python.org/pypi/coverage +.. _setuptools: http://pypi.python.org/pypi/setuptools +.. _Distribute: http://packages.python.org/distribute/ + + +Installing coverage.py is done in the usual ways. You must have `setuptools`_ +or `Distribute`_ installed already, and then you: + +#. Download the appropriate kit from the + `coverage page on the Python Package Index`__. + +#. Run ``python setup.py install``. + +or, use:: + + $ pip install coverage + +or even:: + + $ easy_install coverage + +.. __: coverage_pypi_ + + +Installing from source +---------------------- + +Coverage.py includes a C extension for speed. If you are installing from source, +you may need to install the python-dev support files, for example with:: + + $ sudo apt-get install python-dev + + +Installing on Windows +--------------------- + +For Windows, kits are provided on the `PyPI page`__ for different versions of +Python and different CPU architectures. These kits require that `setuptools`_ be +installed as a pre-requisite, but otherwise are self-contained. They have the +C extension pre-compiled so there's no need to worry about compilers. + +.. __: coverage_pypi_ + + +Checking the installation +------------------------- + +If all went well, you should be able to open a command prompt, and see +coverage installed properly:: + + $ coverage --version + Coverage.py, version 3.6. http://nedbatchelder.com/code/coverage + +You can also invoke coverage as a module:: + + $ python -m coverage --version + Coverage.py, version 3.6. http://nedbatchelder.com/code/coverage diff --git a/doc/source.rst b/doc/source.rst new file mode 100644 index 0000000..aafb976 --- /dev/null +++ b/doc/source.rst @@ -0,0 +1,74 @@ +.. _source: + +======================= +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. +More importantly, when reviewing your coverage reports, you don't want to be +distracted with modules that aren't your concern. + +Coverage.py has a number of ways you can focus it in on the code you care +about. + + +.. _source_execution: + +Execution +--------- + +When running your code, the ``coverage run`` command will by default measure +all code, unless it is part of the Python standard library. + +You can specify source to measure with the ``--source`` command-line switch, +or the ``[run] source`` configuration value. The value is a list of directories +or package names. If specified, only source inside these directories or +packages will be measured. Specifying the source option also enables +coverage.py to report on unexecuted files, since it can search the source tree +for files that haven't been measured at all. + +You can further fine-tune coverage.py's attention with the ``--include`` and +``--omit`` switches (or ``[run] include`` and ``[run] omit`` configuration +values). ``--include`` is a list of filename patterns. If specified, only files +matching those patterns will be measured. ``--omit`` is also a list of filename +patterns, specifying files not to measure. If both ``include`` and ``omit`` +are specified, first the set of files is reduced to only those that match the +include patterns, then any files that match the omit pattern are removed from +the set. + +The ``include`` and ``omit`` filename patterns follow typical shell syntax: +``*`` matches any number of characters and ``?`` matches a single character. +Patterns that start with a wildcard character are used as-is, other patterns +are interpreted relative to the current directory. + +The ``source``, ``include``, and ``omit`` values all work together to determine +the source that will be measured. + + +.. _source_reporting: + +Reporting +--------- + +Once your program is measured, you can specify the source files you want +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 filename 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 +reporting. + +Note that these are ways of specifying files to measure. You can also exclude +individual source lines. See :ref:`excluding` for details. diff --git a/doc/subprocess.rst b/doc/subprocess.rst new file mode 100644 index 0000000..15fa4c2 --- /dev/null +++ b/doc/subprocess.rst @@ -0,0 +1,66 @@ +.. _subprocess: + +====================== +Measuring subprocesses +====================== + +:history: 20100224T201800, new for 3.3. +:history: 20100725T211700, updated for 3.4. + + +Complex test suites may spawn subprocesses to run tests, either to run them in +parallel, or because subprocess behavior is an important part of the system +under test. Measuring coverage in those subprocesses can be tricky because you +have to modify the code spawning the process to invoke coverage.py. + +There's an easier way to do it: coverage.py includes a function, +:func:`coverage.process_startup` designed to be invoked when Python starts. It +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. + +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. + + +Configuring Python for subprocess coverage +------------------------------------------ + +Measuring coverage in subprocesses is a little tricky. When you spawn a +subprocess, you are invoking Python to run your program. Usually, to get +coverage measurement, you have to use coverage.py to run your program. +Your subprocess won't be using coverage.py, so we have to convince Python +to use coverage even when not explicitly invokved. + +To do that, we'll configure Python to run a little coverage.py code when it +starts. That code will look for an environment variable that tells it to +start coverage measurement at the start of the process. + +To arrange all this, you have to do two things: set a value for the +``COVERAGE_PROCESS_START`` environment variable, and then configure Python to +invoke :func:`coverage.process_startup` when Python processes start. + +How you set ``COVERAGE_PROCESS_START`` depends on the details of how you create +subprocesses. As long as the environment variable is visible in your subprocess, +it will work. + +You can configure your Python installation to invoke the ``process_startup`` +function in two ways: + +#. Create or append to sitecustomize.py to add these lines:: + + import coverage + coverage.process_startup() + +#. Create a .pth file in your Python installation containing:: + + import coverage; coverage.process_startup() + +The sitecustomize.py technique is cleaner, but may involve modifying an existing +sitecustomize.py, since there can be only one. If there is no sitecustomize.py +already, you can create it in any directory on the Python path. + +The .pth technique seems like a hack, but works, and is documented behavior. +On the plus side, you can create the file with any name you like so you don't +have to coordinate with other .pth files. On the minus side, you have to create +the file in a system-defined directory, so you may need privileges to write it. diff --git a/doc/trouble.rst b/doc/trouble.rst new file mode 100644 index 0000000..c7693c9 --- /dev/null +++ b/doc/trouble.rst @@ -0,0 +1,88 @@ +.. _trouble: + +========================= +Things that cause trouble +========================= + +:history: 20121231T085200, brand new docs. + +Coverage 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 +with more information. + +I would love to :ref:`hear from you ` if you have information about any of +these problems, even just to explain to me why you want them to start working +properly. + +If your problem isn't discussed here, you can of course search the `coverage.py +bug tracker`_ directly to see if there is some mention of it. + +.. _coverage.py bug tracker: https://bitbucket.org/ned/coveragepy/issues?status=new&status=open + + +Things that don't work +---------------------- + +There are a number of popular modules, packages, and libraries that prevent +coverage.py from working properly: + +* `execv`_, or one of its variants. These end the current program and replace + it with a new one. This doesn't save the collected coverage data, so your + program that calls execv will not be fully measured. A patch for coverage.py + is in `issue 43`_. + +* `multiprocessing`_ launches processes to provide parallelism. These + processes don't get measured by coverage.py. Some possible fixes are + discussed or linked to in `issue 117`_. + +* `gevent`_, which is based on `greenlet`_, and is similar to `eventlet`_. All + of these manipulate the C stack, and therefore confuse coverage.py. + `Issue 149`_ has some pointers to more information. + +* `sys.settrace`_ is the Python feature that coverage.py uses to see what's + happening in your program. If another part of your program is using + sys.settrace, then it will conflict with coverage.py, and it won't be + measured properly. + +.. _execv: http://docs.python.org/library/os#os.execl +.. _multiprocessing: http://docs.python.org/library/multiprocessing.html +.. _gevent: http://www.gevent.org/ +.. _greenlet: http://greenlet.readthedocs.org/ +.. _eventlet: http://eventlet.net/ +.. _sys.settrace: docs.python.org/library/sys.html#sys.settrace +.. _issue 43: https://bitbucket.org/ned/coveragepy/issue/43/coverage-measurement-fails-on-code +.. _issue 117: https://bitbucket.org/ned/coveragepy/issue/117/enable-coverage-measurement-of-code-run-by +.. _issue 149: https://bitbucket.org/ned/coveragepy/issue/149/coverage-gevent-looks-broken + + +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: http://pypi.python.org/pypi/DecoratorTools +.. _TurboGears: http://turbogears.org/ + + +Really obscure things +--------------------- + +* Python 2.5 had a bug (`1569356`_) that could make your program behave + differently when being measured with coverage. This is diagnosed in `issue 51`_. + +.. _issue 51: http://bitbucket.org/ned/coveragepy/issue/51/turbogears-15-test-failing-with-coverage +.. _1569356: http://bugs.python.org/issue1569356 + + +Still having trouble? +--------------------- + +If your problem isn't mentioned here, and isn't already reported in the `coverage.py bug tracker`_, +please :ref:`get in touch with me `, we'll figure out a solution. diff --git a/igor.py b/igor.py new file mode 100644 index 0000000..1d6c2af --- /dev/null +++ b/igor.py @@ -0,0 +1,255 @@ +"""Helper for building, testing, and linting coverage.py. + +To get portability, all these operations are written in Python here instead +of in shell scripts, batch files, or Makefiles. + +""" + +import fnmatch +import glob +import inspect +import os +import platform +import socket +import sys +import zipfile + + +def do_remove_extension(): + """Remove the compiled C extension, no matter what its name.""" + + so_patterns = """ + tracer.so + tracer.*.so + tracer.pyd + """.split() + + for pattern in so_patterns: + pattern = os.path.join("coverage", pattern) + for filename in glob.glob(pattern): + try: + os.remove(filename) + except OSError: + pass + +def run_tests(tracer, *nose_args): + """The actual running of tests.""" + import nose.core + if tracer == "py": + label = "with Python tracer" + else: + label = "with C tracer" + if os.environ.get("COVERAGE_NO_EXTENSION"): + print("Skipping tests, no C extension in this environment") + return + print_banner(label) + os.environ["COVERAGE_TEST_TRACER"] = tracer + nose_args = ["nosetests"] + list(nose_args) + nose.core.main(argv=nose_args) + +def run_tests_with_coverage(tracer, *nose_args): + """Run tests, but with coverage.""" + import coverage + + os.environ['COVERAGE_PROCESS_START'] = os.path.abspath('metacov.ini') + os.environ['COVERAGE_HOME'] = os.getcwd() + + # Create the .pth file that will let us measure coverage in sub-processes. + import nose + pth_dir = os.path.dirname(os.path.dirname(nose.__file__)) + pth_path = os.path.join(pth_dir, "covcov.pth") + pth_file = open(pth_path, "w") + try: + pth_file.write("import coverage; coverage.process_startup()\n") + finally: + pth_file.close() + + version = "%s%s" % sys.version_info[:2] + suffix = "%s_%s_%s" % (version, tracer, socket.gethostname()) + + cov = coverage.coverage(config_file="metacov.ini", data_suffix=suffix) + # Cheap trick: the coverage 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.erase() + cov.start() + + try: + # Re-import coverage to get it coverage tested! I don't understand all + # the mechanics here, but if I don't carry over the imported modules + # (in covmods), then things go haywire (os == None, eventually). + covmods = {} + covdir = os.path.split(coverage.__file__)[0] + # We have to make a list since we'll be deleting in the loop. + modules = list(sys.modules.items()) + for name, mod in modules: + if name.startswith('coverage'): + if getattr(mod, '__file__', "??").startswith(covdir): + covmods[name] = mod + del sys.modules[name] + import coverage # don't warn about re-import: pylint: disable=W0404 + sys.modules.update(covmods) + + # Run nosetests, with the arguments from our command line. + try: + run_tests(tracer, *nose_args) + except SystemExit: + # nose3 seems to raise SystemExit, not sure why? + pass + finally: + cov.stop() + os.remove(pth_path) + + cov.save() + +def do_combine_html(): + """Combine data from a meta-coverage run, and make the HTML report.""" + import coverage + os.environ['COVERAGE_HOME'] = os.getcwd() + cov = coverage.coverage(config_file="metacov.ini") + cov.load() + cov.combine() + cov.save() + cov.html_report() + +def do_test_with_tracer(tracer, *noseargs): + """Run nosetests with a particular tracer.""" + if os.environ.get("COVERAGE_COVERAGE", ""): + return run_tests_with_coverage(tracer, *noseargs) + else: + return run_tests(tracer, *noseargs) + +def do_zip_mods(): + """Build the zipmods.zip file.""" + zf = zipfile.ZipFile("test/zipmods.zip", "w") + zf.write("test/covmodzip1.py", "covmodzip1.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=F0401,E0611,E1101 + import distutils.core + cur_dir = os.getcwd() + os.chdir("test/eggsrc") + distutils.core.run_setup("setup.py", ["--quiet", "bdist_egg"]) + egg = glob.glob("dist/*.egg")[0] + distutils.core.run_setup( + "setup.py", ["--quiet", "easy_install", "--no-deps", "--zip-ok", egg] + ) + os.chdir(cur_dir) + +def do_check_eol(): + """Check files for incorrect newlines and trailing whitespace.""" + + ignore_dirs = [ + '.svn', '.hg', '.tox', '.tox_kits', 'coverage.egg-info', + '_build', 'covtestegg1.egg-info', + ] + checked = set([]) + + def check_file(fname, crlf=True, trail_white=True): + """Check a single file for whitespace abuse.""" + fname = os.path.relpath(fname) + if fname in checked: + return + checked.add(fname) + + line = None + for n, line in enumerate(open(fname, "rb")): + if crlf: + if "\r" in line: + print("%s@%d: CR found" % (fname, n+1)) + return + if trail_white: + line = line[:-1] + if not crlf: + line = line.rstrip('\r') + if line.rstrip() != line: + print("%s@%d: trailing whitespace found" % (fname, n+1)) + return + + if line is not None and not line.strip(): + print("%s: final blank line" % (fname,)) + + def check_files(root, patterns, **kwargs): + """Check a number of files for whitespace abuse.""" + for root, dirs, files in os.walk(root): + for f in files: + fname = os.path.join(root, f) + for p in patterns: + if fnmatch.fnmatch(fname, p): + check_file(fname, **kwargs) + break + for dir_name in ignore_dirs: + if dir_name in dirs: + dirs.remove(dir_name) + + check_files("coverage", ["*.py", "*.c"]) + check_files("coverage/htmlfiles", ["*.html", "*.css", "*.js"]) + check_file("test/farm/html/src/bom.py", crlf=False) + check_files("test", ["*.py"]) + check_files("test", ["*,cover"], trail_white=False) + check_files("test/js", ["*.js", "*.html"]) + check_file("setup.py") + check_file("igor.py") + check_files("doc", ["*.rst"]) + check_files(".", ["*.txt"]) + + +def print_banner(label): + """Print the version of Python.""" + try: + impl = platform.python_implementation() + except AttributeError: + impl = "Python" + + version = platform.python_version() + + if '__pypy__' in sys.builtin_module_names: + pypy_version = sys.pypy_version_info # pylint: disable=E1101 + version += " (pypy %s)" % ".".join([str(v) for v in pypy_version]) + + print('=== %s %s %s (%s) ===' % (impl, version, label, sys.executable)) + + +def do_help(): + """List the available commands""" + items = globals().items() + items.sort() + for name, value in items: + if name.startswith('do_'): + print("%-20s%s" % (name[3:], value.__doc__)) + + +def main(args): + """Main command-line execution for igor. + + Verbs are taken from the command line, and extra words taken as directed + by the arguments needed by the handler. + + """ + while args: + verb = args.pop(0) + handler = globals().get('do_'+verb) + if handler is None: + print("*** No handler for %r" % verb) + return 1 + argspec = inspect.getargspec(handler) + if argspec[1]: + # Handler has *args, give it all the rest of the command line. + handler_args = args + args = [] + else: + # Handler has specific arguments, give it only what it needs. + num_args = len(argspec[0]) + handler_args = args[:num_args] + args = args[num_args:] + ret = handler(*handler_args) + # If a handler returns a failure-like value, stop. + if ret: + return ret + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4c66774 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +nose +mock +tox +pylint +sphinx diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..b14b0bc --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..0b48843 --- /dev/null +++ b/setup.py @@ -0,0 +1,199 @@ +# setup.py for coverage.py + +"""Code coverage measurement for Python + +Coverage.py measures code coverage, typically during test execution. It uses +the code analysis tools and tracing hooks provided in the Python standard +library to determine which lines are executable, and which have been executed. + +Coverage.py runs on Pythons 2.3 through 3.3, and PyPy 1.9. + +Documentation is at `nedbatchelder.com <%s>`_. Code repository and issue +tracker are at `bitbucket.org `_. + +New in 3.6: ``--fail-under``, and >20 bugs closed. + +New in 3.5: Branch coverage exclusions, keyboard shortcuts in HTML report. + +New in 3.4: Better control over source to measure, and unexecuted files +can be reported. + +New in 3.3: .coveragerc files. + +New in 3.2: Branch coverage! +""" + +# This file is used unchanged under all versions of Python, 2.x and 3.x. + +classifiers = """\ +Environment :: Console +Intended Audience :: Developers +License :: OSI Approved :: BSD License +Operating System :: OS Independent +Programming Language :: Python :: 2 +Programming Language :: Python :: 3 +Topic :: Software Development :: Quality Assurance +Topic :: Software Development :: Testing +""" + +# Pull in the tools we need. +import os, sys + +from setuptools import setup +from distutils.core import Extension # pylint: disable=E0611,F0401 +from distutils.command.build_ext import build_ext # pylint: disable=E0611,F0401,C0301 +from distutils import errors # pylint: disable=E0611,F0401 + +# Get or massage our metadata. We exec coverage/version.py so we can avoid +# importing the product code into setup.py. + +doc = __doc__ # __doc__ will be overwritten by version.py. +__version__ = __url__ = "" # Keep pylint happy. + +cov_ver_py = os.path.join(os.path.split(__file__)[0], "coverage/version.py") +version_file = open(cov_ver_py) +try: + exec(compile(version_file.read(), cov_ver_py, 'exec')) +finally: + version_file.close() + +doclines = (doc % __url__).splitlines() +classifier_list = classifiers.splitlines() + +if 'a' in __version__: + devstat = "3 - Alpha" +elif 'b' in __version__: + devstat = "4 - Beta" +else: + devstat = "5 - Production/Stable" +classifier_list.append("Development Status :: " + devstat) + +# Install a script as "coverage", and as "coverage[23]", and as +# "coverage-2.7" (or whatever). +scripts = [ + 'coverage = coverage:main', + 'coverage%d = coverage:main' % sys.version_info[:1], + 'coverage-%d.%d = coverage:main' % sys.version_info[:2], + ] + +# Create the keyword arguments for setup() + +setup_args = dict( + name = 'coverage', + version = __version__, + + packages = [ + 'coverage', + ], + + package_data = { + 'coverage': [ + 'htmlfiles/*.*', + ] + }, + + entry_points = {'console_scripts': scripts}, + + # We need to get HTML assets from our htmlfiles dir. + zip_safe = False, + + author = 'Ned Batchelder and others', + author_email = 'ned@nedbatchelder.com', + description = doclines[0], + long_description = '\n'.join(doclines[2:]), + keywords = 'code coverage testing', + license = 'BSD', + classifiers = classifier_list, + url = __url__, + ) + +# A replacement for the build_ext command which raises a single exception +# if the build fails, so we can fallback nicely. + +ext_errors = ( + errors.CCompilerError, + errors.DistutilsExecError, + errors.DistutilsPlatformError, +) +if sys.platform == 'win32' and sys.version_info > (2, 6): + # 2.6's distutils.msvc9compiler can raise an IOError when failing to + # find the compiler + ext_errors += (IOError,) + +class BuildFailed(Exception): + """Raise this to indicate the C extension wouldn't build.""" + def __init__(self): + Exception.__init__(self) + self.cause = sys.exc_info()[1] # work around py 2/3 different syntax + +class ve_build_ext(build_ext): + """Build C extensions, but fail with a straightforward exception.""" + + def run(self): + """Wrap `run` with `BuildFailed`.""" + try: + build_ext.run(self) + except errors.DistutilsPlatformError: + raise BuildFailed() + + def build_extension(self, ext): + """Wrap `build_extension` with `BuildFailed`.""" + try: + # Uncomment to test compile failures: + # raise errors.CCompilerError("OOPS") + build_ext.build_extension(self, ext) + except ext_errors: + raise BuildFailed() + except ValueError: + # this can happen on Windows 64 bit, see Python issue 7511 + if "'path'" in str(sys.exc_info()[1]): # works with both py 2/3 + raise BuildFailed() + raise + +# There are a few reasons we might not be able to compile the C extension. +# Figure out if we should attempt the C extension or not. + +compile_extension = True + +if sys.platform.startswith('java'): + # Jython can't compile C extensions + compile_extension = False + +if '__pypy__' in sys.builtin_module_names: + # Pypy can't compile C extensions + compile_extension = False + +if compile_extension: + setup_args.update(dict( + ext_modules = [ + Extension("coverage.tracer", sources=["coverage/tracer.c"]) + ], + cmdclass = { + 'build_ext': ve_build_ext, + }, + )) + +# Py3.x-specific details. + +if sys.version_info >= (3, 0): + setup_args.update(dict( + use_2to3 = False, + )) + +def main(): + """Actually invoke setup() with the arguments we built above.""" + # For a variety of reasons, it might not be possible to install the C + # extension. Try it with, and if it fails, try it without. + try: + setup(**setup_args) + except BuildFailed: + msg = "Couldn't install with extension module, trying without it..." + exc = sys.exc_info()[1] + exc_msg = "%s: %s" % (exc.__class__.__name__, exc.cause) + print("**\n** %s\n** %s\n**" % (msg, exc_msg)) + + del setup_args['ext_modules'] + setup(**setup_args) + +if __name__ == '__main__': + main() diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..5a0e30f --- /dev/null +++ b/test/__init__.py @@ -0,0 +1 @@ +"""Automated tests. Run with nosetests.""" diff --git a/test/backtest.py b/test/backtest.py new file mode 100644 index 0000000..b17aa24 --- /dev/null +++ b/test/backtest.py @@ -0,0 +1,49 @@ +"""Add things to old Pythons so I can pretend they are newer, for tests.""" + +# pylint: disable=W0622 +# (Redefining built-in blah) +# The whole point of this file is to redefine built-ins, so shut up about it. + +import os + +# Py2k and 3k don't agree on how to run commands in a subprocess. +try: + import subprocess +except ImportError: + def run_command(cmd, status=0): + """Run a command in a subprocess. + + Returns the exit status code and the combined stdout and stderr. + + """ + _, stdouterr = os.popen4(cmd) + return status, stdouterr.read() + +else: + def run_command(cmd, status=0): + """Run a command in a subprocess. + + Returns the exit status code and the combined stdout and stderr. + + """ + proc = subprocess.Popen(cmd, shell=True, + stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT + ) + output, _ = proc.communicate() + status = proc.returncode # pylint: disable=E1101 + + # Get the output, and canonicalize it to strings with newlines. + if not isinstance(output, str): + output = output.decode('utf-8') + output = output.replace('\r', '') + + return status, output + +# No more execfile in Py3k +try: + execfile = execfile +except NameError: + def execfile(filename, globs): + """A Python 3 implementation of execfile.""" + exec(compile(open(filename).read(), filename, 'exec'), globs) diff --git a/test/backunittest.py b/test/backunittest.py new file mode 100644 index 0000000..30da78e --- /dev/null +++ b/test/backunittest.py @@ -0,0 +1,115 @@ +"""Implementations of unittest features from the future.""" + +import difflib, re, sys, unittest + +from coverage.backward import set # pylint: disable=W0622 + + +def _need(method): + """Do we need to define our own `method` method?""" + return not hasattr(unittest.TestCase, method) + + +class TestCase(unittest.TestCase): + """Just like unittest.TestCase, but with assert methods added. + + Designed to be compatible with 3.1 unittest. Methods are only defined if + the builtin `unittest` doesn't have them. + + """ + if _need('assertTrue'): + def assertTrue(self, exp, msg=None): + """Assert that `exp` is true.""" + if not exp: + self.fail(msg) + + if _need('assertFalse'): + def assertFalse(self, exp, msg=None): + """Assert that `exp` is false.""" + if exp: + self.fail(msg) + + if _need('assertIn'): + def assertIn(self, member, container, msg=None): + """Assert that `member` is in `container`.""" + if member not in container: + msg = msg or ('%r not found in %r' % (member, container)) + self.fail(msg) + + if _need('assertNotIn'): + def assertNotIn(self, member, container, msg=None): + """Assert that `member` is not in `container`.""" + if member in container: + msg = msg or ('%r found in %r' % (member, container)) + self.fail(msg) + + if _need('assertGreater'): + def assertGreater(self, a, b, msg=None): + """Assert that `a` is greater than `b`.""" + if not a > b: + msg = msg or ('%r not greater than %r' % (a, b)) + self.fail(msg) + + if _need('assertRaisesRegexp'): + def assertRaisesRegexp(self, excClass, regexp, callobj, *args, **kw): + """ Just like unittest.TestCase.assertRaises, + but checks that the message is right too. + """ + try: + callobj(*args, **kw) + except excClass: + _, exc, _ = sys.exc_info() + excMsg = str(exc) + if re.search(regexp, excMsg): + # Message provided, and we got the right one: it passes. + return + else: + # Message provided, and it didn't match: fail! + raise self.failureException( + "Right exception, wrong message: " + "%r doesn't match %r" % (excMsg, regexp) + ) + # No need to catch other exceptions: They'll fail the test all by + # themselves! + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException( + "Expected to raise %s, didn't get an exception at all" % + excName + ) + + if _need('assertSameElements'): + def assertSameElements(self, s1, s2): + """Assert that the two arguments are equal as sets.""" + self.assertEqual(set(s1), set(s2)) + + if _need('assertRegexpMatches'): + def assertRegexpMatches(self, text, regex, msg=None): + """Assert that `text` matches `regex`.""" + m = re.search(regex, text) + if not m: + msg = msg or ("%r doesn't match %r" % (text, regex)) + raise self.failureException(msg) + + if _need('assertMultiLineEqual'): + def assertMultiLineEqual(self, first, second, msg=None): + """Assert that two multi-line strings are equal. + + If they aren't, show a nice diff. + + """ + # Adapted from Py3.1 unittest. + self.assertTrue(isinstance(first, str), + 'First argument is not a string') + self.assertTrue(isinstance(second, str), + 'Second argument is not a string') + + if first != second: + message = ''.join(difflib.ndiff(first.splitlines(True), + second.splitlines(True))) + if msg: + message += " : " + msg + self.fail("Multi-line strings are unequal:\n" + message) diff --git a/test/coveragetest.py b/test/coveragetest.py new file mode 100644 index 0000000..6f6217a --- /dev/null +++ b/test/coveragetest.py @@ -0,0 +1,465 @@ +"""Base test case class for coverage testing.""" + +import glob, imp, os, random, shlex, shutil, sys, tempfile, textwrap + +import coverage +from coverage.backward import sorted, StringIO # pylint: disable=W0622 +from coverage.backward import to_bytes +from coverage.control import _TEST_NAME_FILE +from test.backtest import run_command +from test.backunittest import TestCase + +class Tee(object): + """A file-like that writes to all the file-likes it has.""" + + def __init__(self, *files): + """Make a Tee that writes to all the files in `files.`""" + self._files = files + if hasattr(files[0], "encoding"): + self.encoding = files[0].encoding + + def write(self, data): + """Write `data` to all the files.""" + for f in self._files: + f.write(data) + + if 0: + # Use this if you need to use a debugger, though it makes some tests + # fail, I'm not sure why... + def __getattr__(self, name): + return getattr(self._files[0], name) + + +# Status returns for the command line. +OK, ERR = 0, 1 + +class CoverageTest(TestCase): + """A base class for Coverage test cases.""" + + run_in_temp_dir = True + + def setUp(self): + super(CoverageTest, self).setUp() + + if _TEST_NAME_FILE: + f = open(_TEST_NAME_FILE, "w") + f.write("%s_%s" % (self.__class__.__name__, self._testMethodName)) + f.close() + + # Tell newer unittest implementations to print long helpful messages. + self.longMessage = True + + # tearDown will restore the original sys.path + self.old_syspath = sys.path[:] + + if self.run_in_temp_dir: + # Create a temporary directory. + self.noise = str(random.random())[2:] + self.temp_root = os.path.join(tempfile.gettempdir(), 'test_cover') + self.temp_dir = os.path.join(self.temp_root, self.noise) + os.makedirs(self.temp_dir) + self.old_dir = os.getcwd() + os.chdir(self.temp_dir) + + # Modules should be importable from this temp directory. + sys.path.insert(0, '') + + # Keep a counter to make every call to check_coverage unique. + self.n = 0 + + # Record environment variables that we changed with set_environ. + self.environ_undos = {} + + # Capture stdout and stderr so we can examine them in tests. + # nose keeps stdout from littering the screen, so we can safely Tee it, + # but it doesn't capture stderr, so we don't want to Tee stderr to the + # real stderr, since it will interfere with our nice field of dots. + self.old_stdout = sys.stdout + self.captured_stdout = StringIO() + sys.stdout = Tee(sys.stdout, self.captured_stdout) + self.old_stderr = sys.stderr + self.captured_stderr = StringIO() + sys.stderr = self.captured_stderr + + # Record sys.modules here so we can restore it in tearDown. + self.old_modules = dict(sys.modules) + + def tearDown(self): + super(CoverageTest, self).tearDown() + + # Restore the original sys.path. + sys.path = self.old_syspath + + if self.run_in_temp_dir: + # Get rid of the temporary directory. + os.chdir(self.old_dir) + shutil.rmtree(self.temp_root) + + # Restore the environment. + self.undo_environ() + + # Restore stdout and stderr + sys.stdout = self.old_stdout + sys.stderr = self.old_stderr + + self.clean_modules() + + def clean_modules(self): + """Remove any new modules imported during the test run. + + This lets us import the same source files for more than one test. + + """ + for m in [m for m in sys.modules if m not in self.old_modules]: + del sys.modules[m] + + def set_environ(self, name, value): + """Set an environment variable `name` to be `value`. + + The environment variable is set, and record is kept that it was set, + so that `tearDown` can restore its original value. + + """ + if name not in self.environ_undos: + self.environ_undos[name] = os.environ.get(name) + os.environ[name] = value + + def original_environ(self, name, if_missing=None): + """The environment variable `name` from when the test started.""" + if name in self.environ_undos: + ret = self.environ_undos[name] + else: + ret = os.environ.get(name) + if ret is None: + ret = if_missing + return ret + + def undo_environ(self): + """Undo all the changes made by `set_environ`.""" + for name, value in self.environ_undos.items(): + if value is None: + del os.environ[name] + else: + os.environ[name] = value + + def stdout(self): + """Return the data written to stdout during the test.""" + return self.captured_stdout.getvalue() + + def stderr(self): + """Return the data written to stderr during the test.""" + return self.captured_stderr.getvalue() + + def make_file(self, filename, text="", newline=None): + """Create a temp file. + + `filename` is the path to the file, including directories if desired, + and `text` is the content. If `newline` is provided, it is a string + that will be used as the line endings in the created file. + + Returns the path to the file. + + """ + # Tests that call `make_file` should be run in a temp environment. + assert self.run_in_temp_dir + text = textwrap.dedent(text) + if newline: + text = text.replace("\n", newline) + + # Make sure the directories are available. + dirs, _ = os.path.split(filename) + if dirs and not os.path.exists(dirs): + os.makedirs(dirs) + + # Create the file. + f = open(filename, 'wb') + try: + f.write(to_bytes(text)) + finally: + f.close() + + return filename + + def clean_local_file_imports(self): + """Clean up the results of calls to `import_local_file`. + + Use this if you need to `import_local_file` the same file twice in + one test. + + """ + # So that we can re-import files, clean them out first. + self.clean_modules() + # Also have to clean out the .pyc file, since the timestamp + # resolution is only one second, a changed file might not be + # picked up. + for pyc in glob.glob('*.pyc'): + os.remove(pyc) + if os.path.exists("__pycache__"): + shutil.rmtree("__pycache__") + + def import_local_file(self, modname): + """Import a local file as a module. + + Opens a file in the current directory named `modname`.py, imports it + as `modname`, and returns the module object. + + """ + modfile = modname + '.py' + f = open(modfile, 'r') + + for suff in imp.get_suffixes(): + if suff[0] == '.py': + break + try: + # pylint: disable=W0631 + # (Using possibly undefined loop variable 'suff') + mod = imp.load_module(modname, f, modfile, suff) + finally: + f.close() + return mod + + def start_import_stop(self, cov, modname): + """Start coverage, import a file, then stop coverage. + + `cov` is started and stopped, with an `import_local_file` of + `modname` in the middle. + + The imported module is returned. + + """ + cov.start() + try: # pragma: nested + # Import the python file, executing it. + mod = self.import_local_file(modname) + finally: # pragma: nested + # Stop Coverage. + cov.stop() + return mod + + def get_module_name(self): + """Return the module name to use for this test run.""" + # We append self.n because otherwise two calls in one test will use the + # same filename and whether the test works or not depends on the + # timestamps in the .pyc file, so it becomes random whether the second + # call will use the compiled version of the first call's code or not! + modname = 'coverage_test_' + self.noise + str(self.n) + self.n += 1 + return modname + + # 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 assertEqualArcs(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" + self.assertMultiLineEqual(s1, s2, msg) + + def check_coverage(self, text, lines=None, missing="", report="", + excludes=None, partials="", + arcz=None, arcz_missing="", arcz_unpredicted=""): + """Check the coverage measurement of `text`. + + The source `text` is run and measured. `lines` are the line numbers + that are executable, or a list of possible line numbers, any of which + could match. `missing` are the lines not executed, `excludes` are + regexes to match against for excluding lines, and `report` is the text + of the measurement report. + + For arc measurement, `arcz` is a string that can be decoded into arcs + in the code (see `arcz_to_arcs` for the encoding scheme), + `arcz_missing` are the arcs that are not executed, and + `arcs_unpredicted` are the arcs executed in the code, but not deducible + from the code. + + """ + # We write the code into a file so that we can import it. + # Coverage wants to deal with things as modules with file names. + modname = self.get_module_name() + + self.make_file(modname+".py", text) + + arcs = arcs_missing = arcs_unpredicted = None + if arcz is not None: + arcs = self.arcz_to_arcs(arcz) + arcs_missing = self.arcz_to_arcs(arcz_missing or "") + arcs_unpredicted = self.arcz_to_arcs(arcz_unpredicted or "") + + # Start up Coverage. + cov = coverage.coverage(branch=(arcs_missing is not None)) + cov.erase() + for exc in excludes or []: + cov.exclude(exc) + for par in partials or []: + cov.exclude(par, which='partial') + + mod = self.start_import_stop(cov, modname) + + # Clean up our side effects + del sys.modules[modname] + + # Get the analysis results, and check that they are right. + analysis = cov._analyze(mod) + if lines is not None: + if type(lines[0]) == type(1): + # lines is just a list of numbers, it must match the statements + # found in the code. + self.assertEqual(analysis.statements, lines) + else: + # lines is a list of possible line number lists, one of them + # must match. + for line_list in lines: + if analysis.statements == line_list: + break + else: + self.fail("None of the lines choices matched %r" % + analysis.statements + ) + + if type(missing) == type(""): + self.assertEqual(analysis.missing_formatted(), missing) + else: + for missing_list in missing: + if analysis.missing_formatted() == missing_list: + break + else: + self.fail("None of the missing choices matched %r" % + analysis.missing_formatted() + ) + + if arcs is not None: + self.assertEqualArcs( + analysis.arc_possibilities(), arcs, "Possible arcs differ" + ) + + if arcs_missing is not None: + self.assertEqualArcs( + analysis.arcs_missing(), arcs_missing, + "Missing arcs differ" + ) + + if arcs_unpredicted is not None: + self.assertEqualArcs( + analysis.arcs_unpredicted(), arcs_unpredicted, + "Unpredicted arcs differ" + ) + + if report: + frep = StringIO() + cov.report(mod, file=frep) + rep = " ".join(frep.getvalue().split("\n")[2].split()[1:]) + self.assertEqual(report, rep) + + def nice_file(self, *fparts): + """Canonicalize the filename composed of the parts in `fparts`.""" + fname = os.path.join(*fparts) + return os.path.normcase(os.path.abspath(os.path.realpath(fname))) + + def assert_same_files(self, flist1, flist2): + """Assert that `flist1` and `flist2` are the same set of file names.""" + flist1_nice = [self.nice_file(f) for f in flist1] + flist2_nice = [self.nice_file(f) for f in flist2] + self.assertSameElements(flist1_nice, flist2_nice) + + def assert_exists(self, fname): + """Assert that `fname` is a file that exists.""" + msg = "File %r should exist" % fname + self.assert_(os.path.exists(fname), msg) + + def assert_doesnt_exist(self, fname): + """Assert that `fname` is a file that doesn't exist.""" + msg = "File %r shouldn't exist" % fname + self.assert_(not os.path.exists(fname), msg) + + def command_line(self, args, ret=OK, _covpkg=None): + """Run `args` through the command line. + + Use this when you want to run the full coverage machinery, but in the + current process. Exceptions may be thrown from deep in the code. + Asserts that `ret` is returned by `CoverageScript.command_line`. + + Compare with `run_command`. + + Returns None. + + """ + script = coverage.CoverageScript(_covpkg=_covpkg) + ret_actual = script.command_line(shlex.split(args)) + self.assertEqual(ret_actual, ret) + + def run_command(self, cmd): + """Run the command-line `cmd` in a subprocess, and print its output. + + Use this when you need to test the process behavior of coverage. + + Compare with `command_line`. + + Returns the process' stdout text. + + """ + _, output = self.run_command_status(cmd) + return output + + def run_command_status(self, cmd, status=0): + """Run the command-line `cmd` in a subprocess, and print its output. + + Use this when you need to test the process behavior of coverage. + + Compare with `command_line`. + + Returns a pair: the process' exit status and stdout text. + + The `status` argument is returned as the status on older Pythons where + we can't get the actual exit status of the process. + + """ + # Add our test modules directory to PYTHONPATH. I'm sure there's too + # much path munging here, but... + here = os.path.dirname(self.nice_file(coverage.__file__, "..")) + testmods = self.nice_file(here, 'test/modules') + zipfile = self.nice_file(here, 'test/zipmods.zip') + pypath = os.getenv('PYTHONPATH', '') + if pypath: + pypath += os.pathsep + pypath += testmods + os.pathsep + zipfile + self.set_environ('PYTHONPATH', pypath) + + status, output = run_command(cmd, status=status) + print(output) + return status, output diff --git a/test/covmodzip1.py b/test/covmodzip1.py new file mode 100644 index 0000000..3ec4cdc --- /dev/null +++ b/test/covmodzip1.py @@ -0,0 +1,3 @@ +"""covmodzip.py: for putting into a zip file.""" +j = 1 +j += 1 diff --git a/test/eggsrc/egg1/__init__.py b/test/eggsrc/egg1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/eggsrc/egg1/egg1.py b/test/eggsrc/egg1/egg1.py new file mode 100644 index 0000000..3fadde3 --- /dev/null +++ b/test/eggsrc/egg1/egg1.py @@ -0,0 +1,4 @@ +# My egg file! + +walrus = "Eggman" +says = "coo-coo cachoo" diff --git a/test/eggsrc/setup.py b/test/eggsrc/setup.py new file mode 100644 index 0000000..f9b8b9d --- /dev/null +++ b/test/eggsrc/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup + +setup( + name="covtestegg1", + packages=['egg1'], + zip_safe=True, + install_requires=[], + ) diff --git a/test/farm/annotate/annotate_dir.py b/test/farm/annotate/annotate_dir.py new file mode 100644 index 0000000..3e37f9e --- /dev/null +++ b/test/farm/annotate/annotate_dir.py @@ -0,0 +1,7 @@ +copy("src", "run") +run(""" + coverage -e -x multi.py + coverage -a -d out_anno_dir + """, rundir="run") +compare("run/out_anno_dir", "gold_anno_dir", "*,cover", left_extra=True) +clean("run") diff --git a/test/farm/annotate/gold/white.py,cover b/test/farm/annotate/gold/white.py,cover new file mode 100644 index 0000000..36b0b99 --- /dev/null +++ b/test/farm/annotate/gold/white.py,cover @@ -0,0 +1,33 @@ + # 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/test/farm/annotate/gold_anno_dir/a___init__.py,cover b/test/farm/annotate/gold_anno_dir/a___init__.py,cover new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/annotate/gold_anno_dir/a_a.py,cover b/test/farm/annotate/gold_anno_dir/a_a.py,cover new file mode 100644 index 0000000..d0ff3c0 --- /dev/null +++ b/test/farm/annotate/gold_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/test/farm/annotate/gold_anno_dir/b___init__.py,cover b/test/farm/annotate/gold_anno_dir/b___init__.py,cover new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/annotate/gold_anno_dir/b_b.py,cover b/test/farm/annotate/gold_anno_dir/b_b.py,cover new file mode 100644 index 0000000..90d076f --- /dev/null +++ b/test/farm/annotate/gold_anno_dir/b_b.py,cover @@ -0,0 +1,3 @@ +> def b(x): +> msg = "x is %s" % x +> print(msg) diff --git a/test/farm/annotate/gold_anno_dir/multi.py,cover b/test/farm/annotate/gold_anno_dir/multi.py,cover new file mode 100644 index 0000000..2a5c59c --- /dev/null +++ b/test/farm/annotate/gold_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/test/farm/annotate/gold_multi/a/__init__.py,cover b/test/farm/annotate/gold_multi/a/__init__.py,cover new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/annotate/gold_multi/a/a.py,cover b/test/farm/annotate/gold_multi/a/a.py,cover new file mode 100644 index 0000000..fb3f543 --- /dev/null +++ b/test/farm/annotate/gold_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/test/farm/annotate/gold_multi/b/__init__.py,cover b/test/farm/annotate/gold_multi/b/__init__.py,cover new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/annotate/gold_multi/b/b.py,cover b/test/farm/annotate/gold_multi/b/b.py,cover new file mode 100644 index 0000000..a3f5dae --- /dev/null +++ b/test/farm/annotate/gold_multi/b/b.py,cover @@ -0,0 +1,2 @@ +> def b(x): +> print "x is %s" % x diff --git a/test/farm/annotate/gold_multi/multi.py,cover b/test/farm/annotate/gold_multi/multi.py,cover new file mode 100644 index 0000000..2a5c59c --- /dev/null +++ b/test/farm/annotate/gold_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/test/farm/annotate/gold_v24/white.py,cover b/test/farm/annotate/gold_v24/white.py,cover new file mode 100644 index 0000000..bbd8d42 --- /dev/null +++ b/test/farm/annotate/gold_v24/white.py,cover @@ -0,0 +1,33 @@ + # 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/test/farm/annotate/run.py b/test/farm/annotate/run.py new file mode 100644 index 0000000..c645f21 --- /dev/null +++ b/test/farm/annotate/run.py @@ -0,0 +1,7 @@ +copy("src", "out") +run(""" + coverage -e -x white.py + coverage -a white.py + """, rundir="out") +compare("out", "gold", "*,cover") +clean("out") diff --git a/test/farm/annotate/run_multi.py b/test/farm/annotate/run_multi.py new file mode 100644 index 0000000..4e8252e --- /dev/null +++ b/test/farm/annotate/run_multi.py @@ -0,0 +1,7 @@ +copy("src", "out_multi") +run(""" + coverage -e -x multi.py + coverage -a + """, rundir="out_multi") +compare("out_multi", "gold_multi", "*,cover") +clean("out_multi") diff --git a/test/farm/annotate/src/a/__init__.py b/test/farm/annotate/src/a/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/annotate/src/a/a.py b/test/farm/annotate/src/a/a.py new file mode 100644 index 0000000..c2583d1 --- /dev/null +++ b/test/farm/annotate/src/a/a.py @@ -0,0 +1,5 @@ +def a(x): + if x == 1: + print("x is 1") + else: + print("x is not 1") diff --git a/test/farm/annotate/src/b/__init__.py b/test/farm/annotate/src/b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/annotate/src/b/b.py b/test/farm/annotate/src/b/b.py new file mode 100644 index 0000000..625a549 --- /dev/null +++ b/test/farm/annotate/src/b/b.py @@ -0,0 +1,3 @@ +def b(x): + msg = "x is %s" % x + print(msg) diff --git a/test/farm/annotate/src/multi.py b/test/farm/annotate/src/multi.py new file mode 100644 index 0000000..19a6200 --- /dev/null +++ b/test/farm/annotate/src/multi.py @@ -0,0 +1,5 @@ +import a.a +import b.b + +a.a.a(1) +b.b.b(2) diff --git a/test/farm/annotate/src/white.py b/test/farm/annotate/src/white.py new file mode 100644 index 0000000..ecbbd25 --- /dev/null +++ b/test/farm/annotate/src/white.py @@ -0,0 +1,33 @@ +# 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/test/farm/html/gold_a/a.html b/test/farm/html/gold_a/a.html new file mode 100644 index 0000000..c794525 --- /dev/null +++ b/test/farm/html/gold_a/a.html @@ -0,0 +1,95 @@ + + + + + + + + Coverage for a: 67% + + + + + + + + + + + +
+ +

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

+ +
+

# A test file for HTML reporting by coverage. 

+

 

+

if 1 < 2: 

+

    # Needed a < to look at HTML entities. 

+

    a = 3 

+

else: 

+

    a = 4 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_a/index.html b/test/farm/html/gold_a/index.html new file mode 100644 index 0000000..a821e9d --- /dev/null +++ b/test/farm/html/gold_a/index.html @@ -0,0 +1,89 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total31067%
a31067%
+
+ + + + + diff --git a/test/farm/html/gold_b_branch/b.html b/test/farm/html/gold_b_branch/b.html new file mode 100644 index 0000000..0258ad1 --- /dev/null +++ b/test/farm/html/gold_b_branch/b.html @@ -0,0 +1,139 @@ + + + + + + + + Coverage for b: 76% + + + + + + + + + + + +
+ +

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

+ +
+

# A test file for HTML reporting by coverage. 

+

 

+

def one(x): 

+

    # This will be a branch that misses the else. 

+

8    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_way(): 

+

    # for-else can be a three-way branch. 

+

25   26    for i in range(10): 

+

        if i == 3: 

+

            break 

+

    else: 

+

        return 23 

+

    return 17 

+

 

+

three_way() 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_b_branch/index.html b/test/farm/html/gold_b_branch/index.html new file mode 100644 index 0000000..cb6ffa1 --- /dev/null +++ b/test/farm/html/gold_b_branch/index.html @@ -0,0 +1,101 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + b + p + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total16209476%
b16209476%
+
+ + + + + diff --git a/test/farm/html/gold_bom/bom.html b/test/farm/html/gold_bom/bom.html new file mode 100644 index 0000000..1d61a62 --- /dev/null +++ b/test/farm/html/gold_bom/bom.html @@ -0,0 +1,104 @@ + + + + + + + + Coverage for bom: 71% + + + + + + + + + + + + +
+ +

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/test/farm/html/gold_bom/index.html b/test/farm/html/gold_bom/index.html new file mode 100644 index 0000000..8653b23 --- /dev/null +++ b/test/farm/html/gold_bom/index.html @@ -0,0 +1,90 @@ + + + + + Coverage report + + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total72071%
bom72071%
+
+ + + + + diff --git a/test/farm/html/gold_isolatin1/index.html b/test/farm/html/gold_isolatin1/index.html new file mode 100644 index 0000000..6e9f3ca --- /dev/null +++ b/test/farm/html/gold_isolatin1/index.html @@ -0,0 +1,89 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total200100%
isolatin1200100%
+
+ + + + + diff --git a/test/farm/html/gold_isolatin1/isolatin1.html b/test/farm/html/gold_isolatin1/isolatin1.html new file mode 100644 index 0000000..276a6c2 --- /dev/null +++ b/test/farm/html/gold_isolatin1/isolatin1.html @@ -0,0 +1,91 @@ + + + + + + + + Coverage for isolatin1: 100% + + + + + + + + + + + +
+ +

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

+ +
+

# A python source file in another encoding. 

+

# -*- coding: iso8859-1 -*- 

+

 

+

math = "3×4 = 12, ÷2 = 6±0" 

+

assert len(math) == 18 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_1/index.html b/test/farm/html/gold_omit_1/index.html new file mode 100644 index 0000000..5616d01 --- /dev/null +++ b/test/farm/html/gold_omit_1/index.html @@ -0,0 +1,116 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1400100%
m1200100%
m2200100%
m3200100%
main800100%
+
+ + + + + diff --git a/test/farm/html/gold_omit_1/m1.html b/test/farm/html/gold_omit_1/m1.html new file mode 100644 index 0000000..62ba1e0 --- /dev/null +++ b/test/farm/html/gold_omit_1/m1.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m1: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m1a = 1 

+

m1b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_1/m2.html b/test/farm/html/gold_omit_1/m2.html new file mode 100644 index 0000000..d75a5ba --- /dev/null +++ b/test/farm/html/gold_omit_1/m2.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m2: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m2a = 1 

+

m2b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_1/m3.html b/test/farm/html/gold_omit_1/m3.html new file mode 100644 index 0000000..bd99138 --- /dev/null +++ b/test/farm/html/gold_omit_1/m3.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m3: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m3a = 1 

+

m3b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_1/main.html b/test/farm/html/gold_omit_1/main.html new file mode 100644 index 0000000..0394871 --- /dev/null +++ b/test/farm/html/gold_omit_1/main.html @@ -0,0 +1,101 @@ + + + + + + + + Coverage for main: 100% + + + + + + + + + + + +
+ +

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

+ +
+

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/test/farm/html/gold_omit_2/index.html b/test/farm/html/gold_omit_2/index.html new file mode 100644 index 0000000..3ce5bad --- /dev/null +++ b/test/farm/html/gold_omit_2/index.html @@ -0,0 +1,107 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1200100%
m2200100%
m3200100%
main800100%
+
+ + + + + diff --git a/test/farm/html/gold_omit_2/m2.html b/test/farm/html/gold_omit_2/m2.html new file mode 100644 index 0000000..d75a5ba --- /dev/null +++ b/test/farm/html/gold_omit_2/m2.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m2: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m2a = 1 

+

m2b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_2/m3.html b/test/farm/html/gold_omit_2/m3.html new file mode 100644 index 0000000..bd99138 --- /dev/null +++ b/test/farm/html/gold_omit_2/m3.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m3: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m3a = 1 

+

m3b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_2/main.html b/test/farm/html/gold_omit_2/main.html new file mode 100644 index 0000000..0394871 --- /dev/null +++ b/test/farm/html/gold_omit_2/main.html @@ -0,0 +1,101 @@ + + + + + + + + Coverage for main: 100% + + + + + + + + + + + +
+ +

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

+ +
+

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/test/farm/html/gold_omit_3/index.html b/test/farm/html/gold_omit_3/index.html new file mode 100644 index 0000000..fb826bf --- /dev/null +++ b/test/farm/html/gold_omit_3/index.html @@ -0,0 +1,98 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1000100%
m3200100%
main800100%
+
+ + + + + diff --git a/test/farm/html/gold_omit_3/m3.html b/test/farm/html/gold_omit_3/m3.html new file mode 100644 index 0000000..bd99138 --- /dev/null +++ b/test/farm/html/gold_omit_3/m3.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m3: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m3a = 1 

+

m3b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_3/main.html b/test/farm/html/gold_omit_3/main.html new file mode 100644 index 0000000..0394871 --- /dev/null +++ b/test/farm/html/gold_omit_3/main.html @@ -0,0 +1,101 @@ + + + + + + + + Coverage for main: 100% + + + + + + + + + + + +
+ +

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

+ +
+

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/test/farm/html/gold_omit_4/index.html b/test/farm/html/gold_omit_4/index.html new file mode 100644 index 0000000..e437cf1 --- /dev/null +++ b/test/farm/html/gold_omit_4/index.html @@ -0,0 +1,107 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1200100%
m1200100%
m3200100%
main800100%
+
+ + + + + diff --git a/test/farm/html/gold_omit_4/m1.html b/test/farm/html/gold_omit_4/m1.html new file mode 100644 index 0000000..62ba1e0 --- /dev/null +++ b/test/farm/html/gold_omit_4/m1.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m1: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m1a = 1 

+

m1b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_4/m3.html b/test/farm/html/gold_omit_4/m3.html new file mode 100644 index 0000000..bd99138 --- /dev/null +++ b/test/farm/html/gold_omit_4/m3.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m3: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m3a = 1 

+

m3b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_4/main.html b/test/farm/html/gold_omit_4/main.html new file mode 100644 index 0000000..0394871 --- /dev/null +++ b/test/farm/html/gold_omit_4/main.html @@ -0,0 +1,101 @@ + + + + + + + + Coverage for main: 100% + + + + + + + + + + + +
+ +

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

+ +
+

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/test/farm/html/gold_omit_5/index.html b/test/farm/html/gold_omit_5/index.html new file mode 100644 index 0000000..4bde6b7 --- /dev/null +++ b/test/farm/html/gold_omit_5/index.html @@ -0,0 +1,98 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total1000100%
m1200100%
main800100%
+
+ + + + + diff --git a/test/farm/html/gold_omit_5/m1.html b/test/farm/html/gold_omit_5/m1.html new file mode 100644 index 0000000..62ba1e0 --- /dev/null +++ b/test/farm/html/gold_omit_5/m1.html @@ -0,0 +1,85 @@ + + + + + + + + Coverage for m1: 100% + + + + + + + + + + + +
+ +

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

+ +
+

m1a = 1 

+

m1b = 2 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_omit_5/main.html b/test/farm/html/gold_omit_5/main.html new file mode 100644 index 0000000..0394871 --- /dev/null +++ b/test/farm/html/gold_omit_5/main.html @@ -0,0 +1,101 @@ + + + + + + + + Coverage for main: 100% + + + + + + + + + + + +
+ +

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

+ +
+

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/test/farm/html/gold_other/blah_blah_other.html b/test/farm/html/gold_other/blah_blah_other.html new file mode 100644 index 0000000..ab5ae37 --- /dev/null +++ b/test/farm/html/gold_other/blah_blah_other.html @@ -0,0 +1,89 @@ + + + + + + + + Coverage for /home/ned/coverage/trunk/test/farm/html/othersrc/other: 100% + + + + + + + + + + + +
+ +

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

+ +
+

# 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/test/farm/html/gold_other/here.html b/test/farm/html/gold_other/here.html new file mode 100644 index 0000000..1da5bcd --- /dev/null +++ b/test/farm/html/gold_other/here.html @@ -0,0 +1,97 @@ + + + + + + + + Coverage for here: 75% + + + + + + + + + + + +
+ +

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

+ +
+

# A test file for HTML reporting by coverage. 

+

 

+

import other 

+

 

+

if 1 < 2: 

+

    h = 3 

+

else: 

+

    h = 4 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_other/index.html b/test/farm/html/gold_other/index.html new file mode 100644 index 0000000..7665cfe --- /dev/null +++ b/test/farm/html/gold_other/index.html @@ -0,0 +1,98 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total51080%
/home/ned/coverage/trunk/test/farm/html/othersrc/other100100%
here41075%
+
+ + + + + diff --git a/test/farm/html/gold_partial/index.html b/test/farm/html/gold_partial/index.html new file mode 100644 index 0000000..5556150 --- /dev/null +++ b/test/farm/html/gold_partial/index.html @@ -0,0 +1,101 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + b + p + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total80060100%
partial80060100%
+
+ + + + + diff --git a/test/farm/html/gold_partial/partial.html b/test/farm/html/gold_partial/partial.html new file mode 100644 index 0000000..b9640ce --- /dev/null +++ b/test/farm/html/gold_partial/partial.html @@ -0,0 +1,121 @@ + + + + + + + + Coverage for partial: 100% + + + + + + + + + + + +
+ +

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

+ +
+

# 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/test/farm/html/gold_styled/a.html b/test/farm/html/gold_styled/a.html new file mode 100644 index 0000000..c794525 --- /dev/null +++ b/test/farm/html/gold_styled/a.html @@ -0,0 +1,95 @@ + + + + + + + + Coverage for a: 67% + + + + + + + + + + + +
+ +

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

+ +
+

# A test file for HTML reporting by coverage. 

+

 

+

if 1 < 2: 

+

    # Needed a < to look at HTML entities. 

+

    a = 3 

+

else: 

+

    a = 4 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_styled/extra.css b/test/farm/html/gold_styled/extra.css new file mode 100644 index 0000000..46c41fc --- /dev/null +++ b/test/farm/html/gold_styled/extra.css @@ -0,0 +1 @@ +/* Doesn't matter what goes in here, it gets copied. */ diff --git a/test/farm/html/gold_styled/index.html b/test/farm/html/gold_styled/index.html new file mode 100644 index 0000000..a821e9d --- /dev/null +++ b/test/farm/html/gold_styled/index.html @@ -0,0 +1,89 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total31067%
a31067%
+
+ + + + + diff --git a/test/farm/html/gold_styled/style.css b/test/farm/html/gold_styled/style.css new file mode 100644 index 0000000..c40357b --- /dev/null +++ b/test/farm/html/gold_styled/style.css @@ -0,0 +1,275 @@ +/* CSS styles for Coverage. */ +/* Page-wide styles */ +html, body, h1, h2, h3, p, 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.3333em; /* 16/12 */ + } + +table { + border-collapse: collapse; + } + +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: "courier new", 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; +} + +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; +} + +/* Help panel */ +#keyboard_icon { + float: right; + cursor: pointer; +} + +.help_panel { + position: absolute; + background: #ffc; + 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: nowrap; + } + +.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; + font-style: italic; + color: #666; + float: right; + padding-right: .5em; + } +.text p.hide_par span.annotate { + display: none; + } + +/* 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 td.name a:hover { + text-decoration: underline; + color: #000; + } +#index tr.total { + } +#index tr.total td { + font-weight: bold; + border-top: 1px solid #ccc; + border-bottom: none; + } +#index tr.file:hover { + background: #eeeeee; + } diff --git a/test/farm/html/gold_unicode/index.html b/test/farm/html/gold_unicode/index.html new file mode 100644 index 0000000..9ba1bb3 --- /dev/null +++ b/test/farm/html/gold_unicode/index.html @@ -0,0 +1,89 @@ + + + + + Coverage report + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ n + s + m + x + + c   change column sorting +

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedcoverage
Total100100%
unicode100100%
+
+ + + + + diff --git a/test/farm/html/gold_unicode/unicode.html b/test/farm/html/gold_unicode/unicode.html new file mode 100644 index 0000000..518a59a --- /dev/null +++ b/test/farm/html/gold_unicode/unicode.html @@ -0,0 +1,91 @@ + + + + + + + + Coverage for unicode: 100% + + + + + + + + + + + +
+ +

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

+ +
+

# A python source file with exotic characters 

+

# -*- coding: utf-8 -*- 

+

 

+

upside_down = "ʎd˙ǝbɐɹǝʌoɔ" 

+

surrogate = "db40,dd00: x�� 󠄀" 

+ +
+
+ + + + + diff --git a/test/farm/html/gold_x_xml/coverage.xml b/test/farm/html/gold_x_xml/coverage.xml new file mode 100644 index 0000000..912112f --- /dev/null +++ b/test/farm/html/gold_x_xml/coverage.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/test/farm/html/gold_y_xml_branch/coverage.xml b/test/farm/html/gold_y_xml_branch/coverage.xml new file mode 100644 index 0000000..ecbe007 --- /dev/null +++ b/test/farm/html/gold_y_xml_branch/coverage.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/farm/html/othersrc/other.py b/test/farm/html/othersrc/other.py new file mode 100644 index 0000000..6d3f86e --- /dev/null +++ b/test/farm/html/othersrc/other.py @@ -0,0 +1,4 @@ +# 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/test/farm/html/run_a.py b/test/farm/html/run_a.py new file mode 100644 index 0000000..59cc170 --- /dev/null +++ b/test/farm/html/run_a.py @@ -0,0 +1,25 @@ +def html_it(): + """Run coverage and make an HTML report for a.""" + import coverage + cov = coverage.coverage() + cov.start() + import a # pragma: nested + cov.stop() # pragma: nested + cov.html_report(a, directory="../html_a") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_a", "html_a", size_within=10, file_pattern="*.html") +contains("html_a/a.html", + "if 1 < 2", + "    a = 3", + "67%" + ) +contains("html_a/index.html", + "a", + "67%" + ) + +clean("html_a") diff --git a/test/farm/html/run_a_xml_1.py b/test/farm/html/run_a_xml_1.py new file mode 100644 index 0000000..3d18702 --- /dev/null +++ b/test/farm/html/run_a_xml_1.py @@ -0,0 +1,21 @@ +def html_it(): + """Run coverage and make an XML report for a.""" + import coverage + cov = coverage.coverage() + cov.start() + import a # pragma: nested + cov.stop() # pragma: nested + cov.xml_report(a, outfile="../xml_1/coverage.xml") + +import os +if not os.path.exists("xml_1"): + os.makedirs("xml_1") + +runfunc(html_it, rundir="src") + +compare("gold_x_xml", "xml_1", scrubs=[ + (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), + (r' version="[-.\w]+"', ' version="VERSION"'), + (r'/code/coverage/?[-.\w]*', '/code/coverage/VER'), + ]) +clean("xml_1") diff --git a/test/farm/html/run_a_xml_2.py b/test/farm/html/run_a_xml_2.py new file mode 100644 index 0000000..53691ea --- /dev/null +++ b/test/farm/html/run_a_xml_2.py @@ -0,0 +1,21 @@ +def html_it(): + """Run coverage and make an XML report for a.""" + import coverage + cov = coverage.coverage(config_file="run_a_xml_2.ini") + cov.start() + import a # pragma: nested + cov.stop() # pragma: nested + cov.xml_report(a) + +import os +if not os.path.exists("xml_2"): + os.makedirs("xml_2") + +runfunc(html_it, rundir="src") + +compare("gold_x_xml", "xml_2", scrubs=[ + (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), + (r' version="[-.\w]+"', ' version="VERSION"'), + (r'/code/coverage/?[-.\w]*', '/code/coverage/VER'), + ]) +clean("xml_2") diff --git a/test/farm/html/run_b_branch.py b/test/farm/html/run_b_branch.py new file mode 100644 index 0000000..d0955a2 --- /dev/null +++ b/test/farm/html/run_b_branch.py @@ -0,0 +1,28 @@ +def html_it(): + """Run coverage with branches and make an HTML report for b.""" + import coverage + cov = coverage.coverage(branch=True) + cov.start() + import b # pragma: nested + cov.stop() # pragma: nested + cov.html_report(b, directory="../html_b_branch") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_b_branch", "html_b_branch", size_within=10, file_pattern="*.html") +contains("html_b_branch/b.html", + "if x < 2", + "    a = 3", + "70%", + "8", + "exit", + "23   25", + ) +contains("html_b_branch/index.html", + "b", + "70%" + ) + +clean("html_b_branch") diff --git a/test/farm/html/run_bom.py b/test/farm/html/run_bom.py new file mode 100644 index 0000000..c35079b --- /dev/null +++ b/test/farm/html/run_bom.py @@ -0,0 +1,21 @@ +import sys + +def html_it(): + """Run coverage and make an HTML report for bom.py.""" + import coverage + cov = coverage.coverage() + cov.start() + import bom # pragma: nested + cov.stop() # pragma: nested + cov.html_report(bom, directory="../html_bom") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_bom", "html_bom", size_within=10, file_pattern="*.html") +contains("html_bom/bom.html", + ""3×4 = 12, ÷2 = 6±0"", + ) + +clean("html_bom") diff --git a/test/farm/html/run_isolatin1.py b/test/farm/html/run_isolatin1.py new file mode 100644 index 0000000..063e6e0 --- /dev/null +++ b/test/farm/html/run_isolatin1.py @@ -0,0 +1,21 @@ +import sys + +def html_it(): + """Run coverage and make an HTML report for isolatin1.py.""" + import coverage + cov = coverage.coverage() + cov.start() + import isolatin1 # pragma: nested + cov.stop() # pragma: nested + cov.html_report(isolatin1, directory="../html_isolatin1") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_isolatin1", "html_isolatin1", size_within=10, file_pattern="*.html") +contains("html_isolatin1/isolatin1.html", + ""3×4 = 12, ÷2 = 6±0"", + ) + +clean("html_isolatin1") diff --git a/test/farm/html/run_omit_1.py b/test/farm/html/run_omit_1.py new file mode 100644 index 0000000..a5556af --- /dev/null +++ b/test/farm/html/run_omit_1.py @@ -0,0 +1,12 @@ +def html_it(): + """Run coverage and make an HTML report for main.""" + import coverage + cov = coverage.coverage() + cov.start() + import main # pragma: nested + cov.stop() # pragma: nested + cov.html_report(directory="../html_omit_1") + +runfunc(html_it, rundir="src") +compare("gold_omit_1", "html_omit_1", size_within=10, file_pattern="*.html") +clean("html_omit_1") diff --git a/test/farm/html/run_omit_2.py b/test/farm/html/run_omit_2.py new file mode 100644 index 0000000..19f0ebd --- /dev/null +++ b/test/farm/html/run_omit_2.py @@ -0,0 +1,12 @@ +def html_it(): + """Run coverage and make an HTML report for main.""" + import coverage + cov = coverage.coverage() + cov.start() + import main # pragma: nested + cov.stop() # pragma: nested + cov.html_report(directory="../html_omit_2", omit=["m1.py"]) + +runfunc(html_it, rundir="src") +compare("gold_omit_2", "html_omit_2", size_within=10, file_pattern="*.html") +clean("html_omit_2") diff --git a/test/farm/html/run_omit_3.py b/test/farm/html/run_omit_3.py new file mode 100644 index 0000000..87ab8c3 --- /dev/null +++ b/test/farm/html/run_omit_3.py @@ -0,0 +1,12 @@ +def html_it(): + """Run coverage and make an HTML report for main.""" + import coverage + cov = coverage.coverage() + cov.start() + import main # pragma: nested + cov.stop() # pragma: nested + cov.html_report(directory="../html_omit_3", omit=["m1.py", "m2.py"]) + +runfunc(html_it, rundir="src") +compare("gold_omit_3", "html_omit_3", size_within=10, file_pattern="*.html") +clean("html_omit_3") diff --git a/test/farm/html/run_omit_4.py b/test/farm/html/run_omit_4.py new file mode 100644 index 0000000..ede223f --- /dev/null +++ b/test/farm/html/run_omit_4.py @@ -0,0 +1,12 @@ +def html_it(): + """Run coverage and make an HTML report for main.""" + import coverage + cov = coverage.coverage(config_file="omit4.ini") + cov.start() + import main # pragma: nested + cov.stop() # pragma: nested + cov.html_report(directory="../html_omit_4") + +runfunc(html_it, rundir="src") +compare("gold_omit_4", "html_omit_4", size_within=10, file_pattern="*.html") +clean("html_omit_4") diff --git a/test/farm/html/run_omit_5.py b/test/farm/html/run_omit_5.py new file mode 100644 index 0000000..8da5199 --- /dev/null +++ b/test/farm/html/run_omit_5.py @@ -0,0 +1,12 @@ +def html_it(): + """Run coverage and make an HTML report for main.""" + import coverage + cov = coverage.coverage(config_file="omit5.ini") + cov.start() + import main # pragma: nested + cov.stop() # pragma: nested + cov.html_report() + +runfunc(html_it, rundir="src") +compare("gold_omit_5", "html_omit_5", size_within=10, file_pattern="*.html") +clean("html_omit_5") diff --git a/test/farm/html/run_other.py b/test/farm/html/run_other.py new file mode 100644 index 0000000..72bb3ec --- /dev/null +++ b/test/farm/html/run_other.py @@ -0,0 +1,26 @@ +def html_it(): + """Run coverage and make an HTML report for everything.""" + import coverage + cov = coverage.coverage() + cov.start() + import here # pragma: nested + cov.stop() # pragma: nested + cov.html_report(directory="../html_other") + +runfunc(html_it, rundir="src", addtopath="../othersrc") + +# Different platforms will name the "other" file differently. Rename it +import os, glob + +for p in glob.glob("html_other/*_other.html"): + os.rename(p, "html_other/blah_blah_other.html") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_other", "html_other", size_within=10, file_pattern="*.html") +contains("html_other/index.html", + "here", + "other.html'>", "other", + ) + +clean("html_other") diff --git a/test/farm/html/run_partial.py b/test/farm/html/run_partial.py new file mode 100644 index 0000000..41e6ba9 --- /dev/null +++ b/test/farm/html/run_partial.py @@ -0,0 +1,32 @@ +import sys + +def html_it(): + """Run coverage and make an HTML report for partial.""" + import coverage + cov = coverage.coverage(branch=True) + cov.start() + import partial # pragma: nested + cov.stop() # pragma: nested + cov.html_report(partial, directory="../html_partial") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_partial", "html_partial", size_within=10, file_pattern="*.html") +contains("html_partial/partial.html", + "

", + "

", + "

", + # The "if 0" and "if 1" statements are optimized away. + "

", + ) +contains("html_partial/index.html", + "partial", + ) +if sys.version_info >= (2, 4): + contains("html_partial/index.html", + "100%" + ) + +clean("html_partial") diff --git a/test/farm/html/run_styled.py b/test/farm/html/run_styled.py new file mode 100644 index 0000000..ac538ff --- /dev/null +++ b/test/farm/html/run_styled.py @@ -0,0 +1,28 @@ +def html_it(): + """Run coverage and make an HTML report for a.""" + import coverage + cov = coverage.coverage() + cov.start() + import a # pragma: nested + cov.stop() # pragma: nested + cov.html_report(a, directory="../html_styled", extra_css="extra.css") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_styled", "html_styled", size_within=10, file_pattern="*.html") +compare("gold_styled", "html_styled", size_within=10, file_pattern="*.css") +contains("html_styled/a.html", + "", + "if 1 < 2", + "    a = 3", + "67%" + ) +contains("html_styled/index.html", + "", + "a", + "67%" + ) + +clean("html_styled") diff --git a/test/farm/html/run_tabbed.py b/test/farm/html/run_tabbed.py new file mode 100644 index 0000000..0e9b527 --- /dev/null +++ b/test/farm/html/run_tabbed.py @@ -0,0 +1,24 @@ +def html_it(): + """Run coverage and make an HTML report for tabbed.""" + import coverage + cov = coverage.coverage() + cov.start() + import tabbed # pragma: nested + cov.stop() # pragma: nested + cov.html_report(tabbed, directory="../html_tabbed") + +runfunc(html_it, rundir="src") + +# Editors like to change things, make sure our source file still has tabs. +contains("src/tabbed.py", "\tif x:\t\t\t\t\t# look nice") + +contains("html_tabbed/tabbed.html", + ">        if " + "x:" + "                    " + "               " + "# look nice" + ) + +doesnt_contain("html_tabbed/tabbed.html", "\t") +clean("html_tabbed") diff --git a/test/farm/html/run_unicode.py b/test/farm/html/run_unicode.py new file mode 100644 index 0000000..cef26ee --- /dev/null +++ b/test/farm/html/run_unicode.py @@ -0,0 +1,30 @@ +import sys + +def html_it(): + """Run coverage and make an HTML report for unicode.py.""" + import coverage + cov = coverage.coverage() + cov.start() + import unicode # pragma: nested + cov.stop() # pragma: nested + cov.html_report(unicode, directory="../html_unicode") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_unicode", "html_unicode", size_within=10, file_pattern="*.html") +contains("html_unicode/unicode.html", + ""ʎd˙ǝbɐɹǝʌoɔ"", + ) + +if sys.maxunicode == 65535: + contains("html_unicode/unicode.html", + ""db40,dd00: x��"", + ) +else: + contains("html_unicode/unicode.html", + ""db40,dd00: x󠄀"", + ) + +clean("html_unicode") diff --git a/test/farm/html/run_y_xml_branch.py b/test/farm/html/run_y_xml_branch.py new file mode 100644 index 0000000..88a2e44 --- /dev/null +++ b/test/farm/html/run_y_xml_branch.py @@ -0,0 +1,21 @@ +def xml_it(): + """Run coverage and make an XML report for y.""" + import coverage + cov = coverage.coverage(branch=True) + cov.start() + import y # pragma: nested + cov.stop() # pragma: nested + cov.xml_report(y, outfile="../xml_branch/coverage.xml") + +import os +if not os.path.exists("xml_branch"): + os.makedirs("xml_branch") + +runfunc(xml_it, rundir="src") + +compare("gold_y_xml_branch", "xml_branch", scrubs=[ + (r' timestamp="\d+"', ' timestamp="TIMESTAMP"'), + (r' version="[-.\w]+"', ' version="VERSION"'), + (r'/code/coverage/?[-.\w]*', '/code/coverage/VER'), + ]) +clean("xml_branch") diff --git a/test/farm/html/src/a.py b/test/farm/html/src/a.py new file mode 100644 index 0000000..9e71aeb --- /dev/null +++ b/test/farm/html/src/a.py @@ -0,0 +1,7 @@ +# A test file for HTML reporting by coverage. + +if 1 < 2: + # Needed a < to look at HTML entities. + a = 3 +else: + a = 4 diff --git a/test/farm/html/src/b.py b/test/farm/html/src/b.py new file mode 100644 index 0000000..3bf73a9 --- /dev/null +++ b/test/farm/html/src/b.py @@ -0,0 +1,29 @@ +# A test file for HTML reporting by coverage. + +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/test/farm/html/src/bom.py b/test/farm/html/src/bom.py new file mode 100644 index 0000000..2db8b71 --- /dev/null +++ b/test/farm/html/src/bom.py @@ -0,0 +1,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/test/farm/html/src/coverage.xml b/test/farm/html/src/coverage.xml new file mode 100644 index 0000000..128cf75 --- /dev/null +++ b/test/farm/html/src/coverage.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/test/farm/html/src/extra.css b/test/farm/html/src/extra.css new file mode 100644 index 0000000..46c41fc --- /dev/null +++ b/test/farm/html/src/extra.css @@ -0,0 +1 @@ +/* Doesn't matter what goes in here, it gets copied. */ diff --git a/test/farm/html/src/here.py b/test/farm/html/src/here.py new file mode 100644 index 0000000..d0d26ea --- /dev/null +++ b/test/farm/html/src/here.py @@ -0,0 +1,8 @@ +# A test file for HTML reporting by coverage. + +import other + +if 1 < 2: + h = 3 +else: + h = 4 diff --git a/test/farm/html/src/isolatin1.py b/test/farm/html/src/isolatin1.py new file mode 100644 index 0000000..057c097 --- /dev/null +++ b/test/farm/html/src/isolatin1.py @@ -0,0 +1,5 @@ +# A python source file in another encoding. +# -*- coding: iso8859-1 -*- + +math = "3×4 = 12, ÷2 = 6±0" +assert len(math) == 18 diff --git a/test/farm/html/src/m1.py b/test/farm/html/src/m1.py new file mode 100644 index 0000000..927e1f6 --- /dev/null +++ b/test/farm/html/src/m1.py @@ -0,0 +1,2 @@ +m1a = 1 +m1b = 2 diff --git a/test/farm/html/src/m2.py b/test/farm/html/src/m2.py new file mode 100644 index 0000000..ffddf6c --- /dev/null +++ b/test/farm/html/src/m2.py @@ -0,0 +1,2 @@ +m2a = 1 +m2b = 2 diff --git a/test/farm/html/src/m3.py b/test/farm/html/src/m3.py new file mode 100644 index 0000000..395d7d2 --- /dev/null +++ b/test/farm/html/src/m3.py @@ -0,0 +1,2 @@ +m3a = 1 +m3b = 2 diff --git a/test/farm/html/src/main.py b/test/farm/html/src/main.py new file mode 100644 index 0000000..ce89446 --- /dev/null +++ b/test/farm/html/src/main.py @@ -0,0 +1,10 @@ +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/test/farm/html/src/omit4.ini b/test/farm/html/src/omit4.ini new file mode 100644 index 0000000..6821ecd --- /dev/null +++ b/test/farm/html/src/omit4.ini @@ -0,0 +1,2 @@ +[report] +omit = m2.py diff --git a/test/farm/html/src/omit5.ini b/test/farm/html/src/omit5.ini new file mode 100644 index 0000000..7e32b41 --- /dev/null +++ b/test/farm/html/src/omit5.ini @@ -0,0 +1,8 @@ +[report] +omit = + fooey + gooey, m[23]*, kablooey + helloworld + +[html] +directory = ../html_omit_5 diff --git a/test/farm/html/src/partial.py b/test/farm/html/src/partial.py new file mode 100644 index 0000000..8d62f5c --- /dev/null +++ b/test/farm/html/src/partial.py @@ -0,0 +1,18 @@ +# 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/test/farm/html/src/run_a_xml_2.ini b/test/farm/html/src/run_a_xml_2.ini new file mode 100644 index 0000000..8d28f97 --- /dev/null +++ b/test/farm/html/src/run_a_xml_2.ini @@ -0,0 +1,3 @@ +# Put all the XML output in xml_2 +[xml] +output = ../xml_2/coverage.xml diff --git a/test/farm/html/src/tabbed.py b/test/farm/html/src/tabbed.py new file mode 100644 index 0000000..2035852 --- /dev/null +++ b/test/farm/html/src/tabbed.py @@ -0,0 +1,7 @@ +# 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/test/farm/html/src/unicode.py b/test/farm/html/src/unicode.py new file mode 100644 index 0000000..f6a9a05 --- /dev/null +++ b/test/farm/html/src/unicode.py @@ -0,0 +1,5 @@ +# A python source file with exotic characters +# -*- coding: utf-8 -*- + +upside_down = "ʎd˙ǝbɐɹǝʌoɔ" +surrogate = "db40,dd00: x󠄀" diff --git a/test/farm/html/src/y.py b/test/farm/html/src/y.py new file mode 100644 index 0000000..af7c968 --- /dev/null +++ b/test/farm/html/src/y.py @@ -0,0 +1,9 @@ +# A test file for XML reporting by coverage. + +def choice(x): + if x < 2: + return 3 + else: + return 4 + +assert choice(1) == 3 diff --git a/test/farm/run/run_chdir.py b/test/farm/run/run_chdir.py new file mode 100644 index 0000000..f459f50 --- /dev/null +++ b/test/farm/run/run_chdir.py @@ -0,0 +1,12 @@ +copy("src", "out") +run(""" + coverage run chdir.py + coverage -r + """, rundir="out", outfile="stdout.txt") +contains("out/stdout.txt", + "Line One", + "Line Two", + "chdir" + ) +doesnt_contain("out/stdout.txt", "No such file or directory") +clean("out") diff --git a/test/farm/run/run_timid.py b/test/farm/run/run_timid.py new file mode 100644 index 0000000..ce78fff --- /dev/null +++ b/test/farm/run/run_timid.py @@ -0,0 +1,60 @@ +# 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") +run(""" + python showtrace.py none + coverage -e -x showtrace.py regular + coverage -e -x --timid showtrace.py timid + """, rundir="out", outfile="showtraceout.txt") + +# When running without coverage, no trace function +# When running timidly, the trace function is always Python. +contains("out/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/showtraceout.txt", "regular CTracer") +else: + # If the Python trace function is being tested, then regular running will + # also show the Python function. + contains("out/showtraceout.txt", "regular PyTracer") + +# Try the environment variable. +old_opts = os.environ.get('COVERAGE_OPTIONS') +os.environ['COVERAGE_OPTIONS'] = '--timid' + +run(""" + coverage -e -x showtrace.py regular + coverage -e -x --timid showtrace.py timid + """, rundir="out", outfile="showtraceout.txt") + +contains("out/showtraceout.txt", + "none None", + "timid PyTracer", + "regular PyTracer", + ) + +if old_opts: + os.environ['COVERAGE_OPTIONS'] = old_opts +else: + del os.environ['COVERAGE_OPTIONS'] + +clean("out") diff --git a/test/farm/run/run_xxx.py b/test/farm/run/run_xxx.py new file mode 100644 index 0000000..19e94a4 --- /dev/null +++ b/test/farm/run/run_xxx.py @@ -0,0 +1,12 @@ +copy("src", "out") +run(""" + coverage -e -x xxx + coverage -r + """, rundir="out", outfile="stdout.txt") +contains("out/stdout.txt", + "xxx: 3 4 0 7", + "\nxxx ", # The reporting line for xxx + " 7 1 86%" # The reporting data for xxx + ) +doesnt_contain("out/stdout.txt", "No such file or directory") +clean("out") diff --git a/test/farm/run/src/chdir.py b/test/farm/run/src/chdir.py new file mode 100644 index 0000000..6d83492 --- /dev/null +++ b/test/farm/run/src/chdir.py @@ -0,0 +1,4 @@ +import os +print("Line One") +os.chdir("subdir") +print("Line Two") diff --git a/test/farm/run/src/showtrace.py b/test/farm/run/src/showtrace.py new file mode 100644 index 0000000..e97412e --- /dev/null +++ b/test/farm/run/src/showtrace.py @@ -0,0 +1,23 @@ +# 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/test/farm/run/src/subdir/placeholder b/test/farm/run/src/subdir/placeholder new file mode 100644 index 0000000..e69de29 diff --git a/test/farm/run/src/xxx b/test/farm/run/src/xxx new file mode 100644 index 0000000..8f727f0 --- /dev/null +++ b/test/farm/run/src/xxx @@ -0,0 +1,8 @@ +# 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/test/js/index.html b/test/js/index.html new file mode 100644 index 0000000..60bdb30 --- /dev/null +++ b/test/js/index.html @@ -0,0 +1,52 @@ + + + + Coverage.py Javascript Test Suite + + + + + + + + + + + + + + + + + + + + + +

Coverage.py Javascript Test Suite

+

+
+

+
    +
    + + diff --git a/test/js/tests.js b/test/js/tests.js new file mode 100644 index 0000000..73b4ce2 --- /dev/null +++ b/test/js/tests.js @@ -0,0 +1,204 @@ +// Tests of coverage.py HTML report chunk navigation. +/*global coverage, test, module, equals, jQuery, $ */ + +// Test helpers + +function selection_is(sel) { + raw_selection_is(sel, true); +} + +function raw_selection_is(sel, check_highlight) { + var beg = sel[0], end = sel[1]; + equals(coverage.sel_begin, beg); + equals(coverage.sel_end, end); + if (check_highlight) { + equals(coverage.code_container().find(".highlight").length, end-beg); + } +} + +function build_fixture(spec) { + var i, data; + $("#fixture-template").tmpl().appendTo("#qunit-fixture"); + for (i = 0; i < spec.length; i++) { + data = {number: i+1, klass: spec.substr(i, 1)}; + $("#lineno-template").tmpl(data).appendTo("#qunit-fixture .linenos"); + $("#text-template").tmpl(data).appendTo("#qunit-fixture .text"); + } + coverage.pyfile_ready(jQuery); +} + +// Tests + +// Zero-chunk tests + +module("Zero-chunk navigation", { + setup: function () { + build_fixture("wwww"); + } +}); + +test("set_sel defaults", function () { + coverage.set_sel(2); + equals(coverage.sel_begin, 2); + equals(coverage.sel_end, 3); +}); + +test("No first chunk to select", function () { + coverage.to_first_chunk(); +}); + +// One-chunk tests + +$.each([ + ['rrrrr', [1,6]], + ['r', [1,2]], + ['wwrrrr', [3,7]], + ['wwrrrrww', [3,7]], + ['rrrrww', [1,5]] +], function (i, params) { + + // Each of these tests uses a fixture with one highlighted chunks. + var id = params[0]; + var c1 = params[1]; + + module("One-chunk navigation - " + id, { + setup: function () { + build_fixture(id); + } + }); + + test("First chunk", function () { + coverage.to_first_chunk(); + selection_is(c1); + }); + + test("Next chunk is first chunk", function () { + coverage.to_next_chunk(); + selection_is(c1); + }); + + test("There is no next chunk", function () { + coverage.to_first_chunk(); + coverage.to_next_chunk(); + selection_is(c1); + }); + + test("There is no prev chunk", function () { + coverage.to_first_chunk(); + coverage.to_prev_chunk(); + selection_is(c1); + }); +}); + +// Two-chunk tests + +$.each([ + ['rrwwrrrr', [1,3], [5,9]], + ['rb', [1,2], [2,3]], + ['rbbbbbbbbbb', [1,2], [2,12]], + ['rrrrrrrrrrb', [1,11], [11,12]], + ['wrrwrrrrw', [2,4], [5,9]], + ['rrrbbb', [1,4], [4,7]] +], function (i, params) { + + // Each of these tests uses a fixture with two highlighted chunks. + var id = params[0]; + var c1 = params[1]; + var c2 = params[2]; + + module("Two-chunk navigation - " + id, { + setup: function () { + build_fixture(id); + } + }); + + test("First chunk", function () { + coverage.to_first_chunk(); + selection_is(c1); + }); + + test("Next chunk is first chunk", function () { + coverage.to_next_chunk(); + selection_is(c1); + }); + + test("Move to next chunk", function () { + coverage.to_first_chunk(); + coverage.to_next_chunk(); + selection_is(c2); + }); + + test("Move to first chunk", function () { + coverage.to_first_chunk(); + coverage.to_next_chunk(); + coverage.to_first_chunk(); + selection_is(c1); + }); + + test("Move to previous chunk", function () { + coverage.to_first_chunk(); + coverage.to_next_chunk(); + coverage.to_prev_chunk(); + selection_is(c1); + }); + + test("Next doesn't move after last chunk", function () { + coverage.to_first_chunk(); + coverage.to_next_chunk(); + coverage.to_next_chunk(); + selection_is(c2); + }); + + test("Prev doesn't move before first chunk", function () { + coverage.to_first_chunk(); + coverage.to_next_chunk(); + coverage.to_prev_chunk(); + coverage.to_prev_chunk(); + selection_is(c1); + }); + +}); + +module("Miscellaneous"); + +test("Jump from a line selected", function () { + build_fixture("rrwwrr"); + coverage.set_sel(3); + coverage.to_next_chunk(); + selection_is([5,7]); +}); + +// Tests of select_line_or_chunk. + +$.each([ + // The data for each test: a spec for the fixture to build, and an array + // of the selection that will be selected by select_line_or_chunk for + // each line in the fixture. + ['rrwwrr', [[1,3], [1,3], [3,4], [4,5], [5,7], [5,7]]], + ['rb', [[1,2], [2,3]]], + ['r', [[1,2]]], + ['w', [[1,2]]], + ['www', [[1,2], [2,3], [3,4]]], + ['wwwrrr', [[1,2], [2,3], [3,4], [4,7], [4,7], [4,7]]], + ['rrrwww', [[1,4], [1,4], [1,4], [4,5], [5,6], [6,7]]], + ['rrrbbb', [[1,4], [1,4], [1,4], [4,7], [4,7], [4,7]]] +], function (i, params) { + + // Each of these tests uses a fixture with two highlighted chunks. + var id = params[0]; + var sels = params[1]; + + module("Select line or chunk - " + id, { + setup: function () { + build_fixture(id); + } + }); + + $.each(sels, function (i, sel) { + i++; + test("Select line " + i, function () { + coverage.select_line_or_chunk(i); + raw_selection_is(sel); + }); + }); +}); diff --git a/test/modules/aa/__init__.py b/test/modules/aa/__init__.py new file mode 100644 index 0000000..77593d8 --- /dev/null +++ b/test/modules/aa/__init__.py @@ -0,0 +1 @@ +# aa diff --git a/test/modules/aa/afile.odd.py b/test/modules/aa/afile.odd.py new file mode 100644 index 0000000..c6f49e1 --- /dev/null +++ b/test/modules/aa/afile.odd.py @@ -0,0 +1 @@ +# afile.odd.py diff --git a/test/modules/aa/afile.py b/test/modules/aa/afile.py new file mode 100644 index 0000000..3f0e38d --- /dev/null +++ b/test/modules/aa/afile.py @@ -0,0 +1 @@ +# afile.py diff --git a/test/modules/aa/bb.odd/bfile.py b/test/modules/aa/bb.odd/bfile.py new file mode 100644 index 0000000..9087540 --- /dev/null +++ b/test/modules/aa/bb.odd/bfile.py @@ -0,0 +1 @@ +# bfile.py diff --git a/test/modules/aa/bb/__init__.py b/test/modules/aa/bb/__init__.py new file mode 100644 index 0000000..ffbe624 --- /dev/null +++ b/test/modules/aa/bb/__init__.py @@ -0,0 +1 @@ +# bb diff --git a/test/modules/aa/bb/bfile.odd.py b/test/modules/aa/bb/bfile.odd.py new file mode 100644 index 0000000..b45cba8 --- /dev/null +++ b/test/modules/aa/bb/bfile.odd.py @@ -0,0 +1 @@ +# bfile.odd.py diff --git a/test/modules/aa/bb/bfile.py b/test/modules/aa/bb/bfile.py new file mode 100644 index 0000000..9087540 --- /dev/null +++ b/test/modules/aa/bb/bfile.py @@ -0,0 +1 @@ +# bfile.py diff --git a/test/modules/aa/bb/cc/__init__.py b/test/modules/aa/bb/cc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/modules/aa/bb/cc/cfile.py b/test/modules/aa/bb/cc/cfile.py new file mode 100644 index 0000000..7976475 --- /dev/null +++ b/test/modules/aa/bb/cc/cfile.py @@ -0,0 +1 @@ +# cfile.py diff --git a/test/modules/aa/zfile.py b/test/modules/aa/zfile.py new file mode 100644 index 0000000..924f9b7 --- /dev/null +++ b/test/modules/aa/zfile.py @@ -0,0 +1 @@ +# zfile.py diff --git a/test/modules/covmod1.py b/test/modules/covmod1.py new file mode 100644 index 0000000..b3f5e5f --- /dev/null +++ b/test/modules/covmod1.py @@ -0,0 +1,3 @@ +# covmod1.py: Simplest module for testing. +i = 1 +i += 1 diff --git a/test/modules/pkg1/__init__.py b/test/modules/pkg1/__init__.py new file mode 100644 index 0000000..2dfeb9c --- /dev/null +++ b/test/modules/pkg1/__init__.py @@ -0,0 +1,3 @@ +# This __init__.py has a module-level docstring, which is counted as a +# statement. +"""A simple package for testing with.""" diff --git a/test/modules/pkg1/__main__.py b/test/modules/pkg1/__main__.py new file mode 100644 index 0000000..66ce595 --- /dev/null +++ b/test/modules/pkg1/__main__.py @@ -0,0 +1,3 @@ +# Used in the tests for run_python_module +import sys +print("pkg1.__main__: passed %s" % sys.argv[1]) diff --git a/test/modules/pkg1/p1a.py b/test/modules/pkg1/p1a.py new file mode 100644 index 0000000..be5fcdd --- /dev/null +++ b/test/modules/pkg1/p1a.py @@ -0,0 +1,5 @@ +import os, sys + +# Invoke functions in os and sys so we can see if we measure code there. +x = sys.getcheckinterval() +y = os.getcwd() diff --git a/test/modules/pkg1/p1b.py b/test/modules/pkg1/p1b.py new file mode 100644 index 0000000..59d6fb5 --- /dev/null +++ b/test/modules/pkg1/p1b.py @@ -0,0 +1,3 @@ +x = 1 +y = 2 +z = 3 diff --git a/test/modules/pkg1/p1c.py b/test/modules/pkg1/p1c.py new file mode 100644 index 0000000..a9aeef0 --- /dev/null +++ b/test/modules/pkg1/p1c.py @@ -0,0 +1,3 @@ +a = 1 +b = 2 +c = 3 diff --git a/test/modules/pkg1/runmod2.py b/test/modules/pkg1/runmod2.py new file mode 100644 index 0000000..b52964c --- /dev/null +++ b/test/modules/pkg1/runmod2.py @@ -0,0 +1,3 @@ +# Used in the tests for run_python_module +import sys +print("runmod2: passed %s" % sys.argv[1]) diff --git a/test/modules/pkg1/sub/__init__.py b/test/modules/pkg1/sub/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/modules/pkg1/sub/__main__.py b/test/modules/pkg1/sub/__main__.py new file mode 100644 index 0000000..b5be9f1 --- /dev/null +++ b/test/modules/pkg1/sub/__main__.py @@ -0,0 +1,3 @@ +# Used in the tests for run_python_module +import sys +print("pkg1.sub.__main__: passed %s" % sys.argv[1]) diff --git a/test/modules/pkg1/sub/ps1a.py b/test/modules/pkg1/sub/ps1a.py new file mode 100644 index 0000000..4b6a15c --- /dev/null +++ b/test/modules/pkg1/sub/ps1a.py @@ -0,0 +1,3 @@ +d = 1 +e = 2 +f = 3 diff --git a/test/modules/pkg1/sub/runmod3.py b/test/modules/pkg1/sub/runmod3.py new file mode 100644 index 0000000..3a1ad15 --- /dev/null +++ b/test/modules/pkg1/sub/runmod3.py @@ -0,0 +1,3 @@ +# Used in the tests for run_python_module +import sys +print("runmod3: passed %s" % sys.argv[1]) diff --git a/test/modules/pkg2/__init__.py b/test/modules/pkg2/__init__.py new file mode 100644 index 0000000..090efbf --- /dev/null +++ b/test/modules/pkg2/__init__.py @@ -0,0 +1,2 @@ +# This is an __init__.py file, with no executable statements in it. +# This comment shouldn't confuse the parser. diff --git a/test/modules/pkg2/p2a.py b/test/modules/pkg2/p2a.py new file mode 100644 index 0000000..b606711 --- /dev/null +++ b/test/modules/pkg2/p2a.py @@ -0,0 +1,3 @@ +q = 1 +r = 1 +s = 1 diff --git a/test/modules/pkg2/p2b.py b/test/modules/pkg2/p2b.py new file mode 100644 index 0000000..7a34e2c --- /dev/null +++ b/test/modules/pkg2/p2b.py @@ -0,0 +1,3 @@ +t = 1 +u = 1 +v = 1 diff --git a/test/modules/runmod1.py b/test/modules/runmod1.py new file mode 100644 index 0000000..671d81e --- /dev/null +++ b/test/modules/runmod1.py @@ -0,0 +1,3 @@ +# Used in the tests for run_python_module +import sys +print("runmod1: passed %s" % sys.argv[1]) diff --git a/test/modules/usepkgs.py b/test/modules/usepkgs.py new file mode 100644 index 0000000..93c7d90 --- /dev/null +++ b/test/modules/usepkgs.py @@ -0,0 +1,4 @@ +import pkg1.p1a, pkg1.p1b +import pkg2.p2a, pkg2.p2b +import othermods.othera, othermods.otherb +import othermods.sub.osa, othermods.sub.osb diff --git a/test/moremodules/othermods/__init__.py b/test/moremodules/othermods/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/moremodules/othermods/othera.py b/test/moremodules/othermods/othera.py new file mode 100644 index 0000000..7889692 --- /dev/null +++ b/test/moremodules/othermods/othera.py @@ -0,0 +1,2 @@ +o = 1 +p = 2 diff --git a/test/moremodules/othermods/otherb.py b/test/moremodules/othermods/otherb.py new file mode 100644 index 0000000..2bd8a44 --- /dev/null +++ b/test/moremodules/othermods/otherb.py @@ -0,0 +1,2 @@ +q = 3 +r = 4 diff --git a/test/moremodules/othermods/sub/__init__.py b/test/moremodules/othermods/sub/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/moremodules/othermods/sub/osa.py b/test/moremodules/othermods/sub/osa.py new file mode 100644 index 0000000..0139d28 --- /dev/null +++ b/test/moremodules/othermods/sub/osa.py @@ -0,0 +1,2 @@ +s = 5 +t = 6 diff --git a/test/moremodules/othermods/sub/osb.py b/test/moremodules/othermods/sub/osb.py new file mode 100644 index 0000000..b024b14 --- /dev/null +++ b/test/moremodules/othermods/sub/osb.py @@ -0,0 +1,2 @@ +u = 7 +v = 8 diff --git a/test/osinfo.py b/test/osinfo.py new file mode 100644 index 0000000..25c3a7c --- /dev/null +++ b/test/osinfo.py @@ -0,0 +1,71 @@ +"""OS information for testing.""" + +import sys + +if sys.version_info >= (2, 5) and sys.platform == 'win32': + # Windows implementation + def process_ram(): + """How much RAM is this process using? (Windows)""" + import ctypes + # lifted from: + # lists.ubuntu.com/archives/bazaar-commits/2009-February/011990.html + class PROCESS_MEMORY_COUNTERS_EX(ctypes.Structure): + """Used by GetProcessMemoryInfo""" + _fields_ = [('cb', ctypes.c_ulong), + ('PageFaultCount', ctypes.c_ulong), + ('PeakWorkingSetSize', ctypes.c_size_t), + ('WorkingSetSize', ctypes.c_size_t), + ('QuotaPeakPagedPoolUsage', ctypes.c_size_t), + ('QuotaPagedPoolUsage', ctypes.c_size_t), + ('QuotaPeakNonPagedPoolUsage', ctypes.c_size_t), + ('QuotaNonPagedPoolUsage', ctypes.c_size_t), + ('PagefileUsage', ctypes.c_size_t), + ('PeakPagefileUsage', ctypes.c_size_t), + ('PrivateUsage', ctypes.c_size_t), + ] + + mem_struct = PROCESS_MEMORY_COUNTERS_EX() + ret = ctypes.windll.psapi.GetProcessMemoryInfo( + ctypes.windll.kernel32.GetCurrentProcess(), + ctypes.byref(mem_struct), + ctypes.sizeof(mem_struct) + ) + if not ret: + return 0 + return mem_struct.PrivateUsage + +elif sys.platform == 'linux2': + # Linux implementation + import os + + _scale = {'kb': 1024, 'mb': 1024*1024} + + def _VmB(key): + """Read the /proc/PID/status file to find memory use.""" + try: + # get pseudo file /proc//status + t = open('/proc/%d/status' % os.getpid()) + try: + v = t.read() + finally: + t.close() + except IOError: + return 0 # non-Linux? + # get VmKey line e.g. 'VmRSS: 9999 kB\n ...' + i = v.index(key) + v = v[i:].split(None, 3) + if len(v) < 3: + return 0 # invalid format? + # convert Vm value to bytes + return int(float(v[1]) * _scale[v[2].lower()]) + + def process_ram(): + """How much RAM is this process using? (Linux implementation)""" + return _VmB('VmRSS') + + +else: + # Don't have an implementation, at least satisfy the interface. + def process_ram(): + """How much RAM is this process using? (placebo implementation)""" + return 0 diff --git a/test/qunit/jquery.tmpl.min.js b/test/qunit/jquery.tmpl.min.js new file mode 100644 index 0000000..7438b2c --- /dev/null +++ b/test/qunit/jquery.tmpl.min.js @@ -0,0 +1,10 @@ +/* + * jQuery Templates Plugin 1.0.0pre + * http://github.com/jquery/jquery-tmpl + * Requires jQuery 1.4.2 + * + * Copyright Software Freedom Conservancy, Inc. + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + */ +(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},i=0,c=0,l=[];function g(g,d,h,e){var c={data:e||(e===0||e===false)?e:d?d.data:{},_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};g&&a.extend(c,g,{nodes:[],parent:d});if(h){c.tmpl=h;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++i;(l.length?f:b)[i]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a(i[h])[d](k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,m,k){if(d[0]&&a.isArray(d[0])){var g=a.makeArray(arguments),h=d[0],j=h.length,i=0,f;while(i").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){__=__.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(__,$1,$2);__=[];",close:"call=$item.calls();__=call._.concat($item.wrap(call,__));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){__.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){__.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function j(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:j(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=k(c).concat(b);if(d)b=b.concat(k(d))});return b?b:k(c)}function k(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,__=[],$data=$item.data;with($data){__.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,k,g,b,c,d){var j=a.tmpl.tag[k],i,e,f;if(!j)throw"Unknown template tag: "+k;i=j._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=h(b);d=d?","+h(d)+")":c?")":"";e=c?b.indexOf(".")>-1?b+h(c):"("+b+").call($item"+d:b;f=c?e:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else f=e=i.$1||"null";g=h(g);return"');"+j[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(f).split("$1").join(e).split("$2").join(g||i.$2||"")+"__.push('"})+"');}return __;")}function n(c,b){c._wrap=j(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function h(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,h;for(e=0,p=o.length;e=0;h--)m(j[h]);m(k)}function m(j){var p,h=j,k,e,m;if(m=j.getAttribute(d)){while(h.parentNode&&(h=h.parentNode).nodeType===1&&!(p=h.getAttribute(d)));if(p!==m){h=h.parentNode?h.nodeType===11?0:h.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[h]||f[h]);e.key=++i;b[i]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;h=a.data(j.parentNode,"tmplItem");h=h?h.key:0}if(e){k=e;while(k&&k.key!=h){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery); \ No newline at end of file diff --git a/test/qunit/qunit.css b/test/qunit/qunit.css new file mode 100644 index 0000000..b3c6db5 --- /dev/null +++ b/test/qunit/qunit.css @@ -0,0 +1,225 @@ +/** + * QUnit - A JavaScript Unit Testing Framework + * + * http://docs.jquery.com/QUnit + * + * Copyright (c) 2011 John Resig, Jörn Zaefferer + * Dual licensed under the MIT (MIT-LICENSE.txt) + * or GPL (GPL-LICENSE.txt) licenses. + */ + +/** Font Family and Sizes */ + +#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { + font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; +} + +#qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } +#qunit-tests { font-size: smaller; } + + +/** Resets */ + +#qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { + margin: 0; + padding: 0; +} + + +/** Header */ + +#qunit-header { + padding: 0.5em 0 0.5em 1em; + + color: #8699a4; + background-color: #0d3349; + + font-size: 1.5em; + line-height: 1em; + font-weight: normal; + + border-radius: 15px 15px 0 0; + -moz-border-radius: 15px 15px 0 0; + -webkit-border-top-right-radius: 15px; + -webkit-border-top-left-radius: 15px; +} + +#qunit-header a { + text-decoration: none; + color: #c2ccd1; +} + +#qunit-header a:hover, +#qunit-header a:focus { + color: #fff; +} + +#qunit-banner { + height: 5px; +} + +#qunit-testrunner-toolbar { + padding: 0.5em 0 0.5em 2em; + color: #5E740B; + background-color: #eee; +} + +#qunit-userAgent { + padding: 0.5em 0 0.5em 2.5em; + background-color: #2b81af; + color: #fff; + text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; +} + + +/** Tests: Pass/Fail */ + +#qunit-tests { + list-style-position: inside; +} + +#qunit-tests li { + padding: 0.4em 0.5em 0.4em 2.5em; + border-bottom: 1px solid #fff; + list-style-position: inside; +} + +#qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { + display: none; +} + +#qunit-tests li strong { + cursor: pointer; +} + +#qunit-tests li a { + padding: 0.5em; + color: #c2ccd1; + text-decoration: none; +} +#qunit-tests li a:hover, +#qunit-tests li a:focus { + color: #000; +} + +#qunit-tests ol { + margin-top: 0.5em; + padding: 0.5em; + + background-color: #fff; + + border-radius: 15px; + -moz-border-radius: 15px; + -webkit-border-radius: 15px; + + box-shadow: inset 0px 2px 13px #999; + -moz-box-shadow: inset 0px 2px 13px #999; + -webkit-box-shadow: inset 0px 2px 13px #999; +} + +#qunit-tests table { + border-collapse: collapse; + margin-top: .2em; +} + +#qunit-tests th { + text-align: right; + vertical-align: top; + padding: 0 .5em 0 0; +} + +#qunit-tests td { + vertical-align: top; +} + +#qunit-tests pre { + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; +} + +#qunit-tests del { + background-color: #e0f2be; + color: #374e0c; + text-decoration: none; +} + +#qunit-tests ins { + background-color: #ffcaca; + color: #500; + text-decoration: none; +} + +/*** Test Counts */ + +#qunit-tests b.counts { color: black; } +#qunit-tests b.passed { color: #5E740B; } +#qunit-tests b.failed { color: #710909; } + +#qunit-tests li li { + margin: 0.5em; + padding: 0.4em 0.5em 0.4em 0.5em; + background-color: #fff; + border-bottom: none; + list-style-position: inside; +} + +/*** Passing Styles */ + +#qunit-tests li li.pass { + color: #5E740B; + background-color: #fff; + border-left: 26px solid #C6E746; +} + +#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } +#qunit-tests .pass .test-name { color: #366097; } + +#qunit-tests .pass .test-actual, +#qunit-tests .pass .test-expected { color: #999999; } + +#qunit-banner.qunit-pass { background-color: #C6E746; } + +/*** Failing Styles */ + +#qunit-tests li li.fail { + color: #710909; + background-color: #fff; + border-left: 26px solid #EE5757; +} + +#qunit-tests > li:last-child { + border-radius: 0 0 15px 15px; + -moz-border-radius: 0 0 15px 15px; + -webkit-border-bottom-right-radius: 15px; + -webkit-border-bottom-left-radius: 15px; +} + +#qunit-tests .fail { color: #000000; background-color: #EE5757; } +#qunit-tests .fail .test-name, +#qunit-tests .fail .module-name { color: #000000; } + +#qunit-tests .fail .test-actual { color: #EE5757; } +#qunit-tests .fail .test-expected { color: green; } + +#qunit-banner.qunit-fail { background-color: #EE5757; } + + +/** Result */ + +#qunit-testresult { + padding: 0.5em 0.5em 0.5em 2.5em; + + color: #2b81af; + background-color: #D2E0E6; + + border-bottom: 1px solid white; +} + +/** Fixture */ + +#qunit-fixture { + position: absolute; + top: -10000px; + left: -10000px; +} diff --git a/test/qunit/qunit.js b/test/qunit/qunit.js new file mode 100644 index 0000000..e00cca9 --- /dev/null +++ b/test/qunit/qunit.js @@ -0,0 +1,1448 @@ +/** + * QUnit - A JavaScript Unit Testing Framework + * + * http://docs.jquery.com/QUnit + * + * Copyright (c) 2011 John Resig, Jörn Zaefferer + * Dual licensed under the MIT (MIT-LICENSE.txt) + * or GPL (GPL-LICENSE.txt) licenses. + */ + +(function(window) { + +var defined = { + setTimeout: typeof window.setTimeout !== "undefined", + sessionStorage: (function() { + try { + return !!sessionStorage.getItem; + } catch(e){ + return false; + } + })() +}; + +var testId = 0; + +var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { + this.name = name; + this.testName = testName; + this.expected = expected; + this.testEnvironmentArg = testEnvironmentArg; + this.async = async; + this.callback = callback; + this.assertions = []; +}; +Test.prototype = { + init: function() { + var tests = id("qunit-tests"); + if (tests) { + var b = document.createElement("strong"); + b.innerHTML = "Running " + this.name; + var li = document.createElement("li"); + li.appendChild( b ); + li.className = "running"; + li.id = this.id = "test-output" + testId++; + tests.appendChild( li ); + } + }, + setup: function() { + if (this.module != config.previousModule) { + if ( config.previousModule ) { + QUnit.moduleDone( { + name: config.previousModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + } ); + } + config.previousModule = this.module; + config.moduleStats = { all: 0, bad: 0 }; + QUnit.moduleStart( { + name: this.module + } ); + } + + config.current = this; + this.testEnvironment = extend({ + setup: function() {}, + teardown: function() {} + }, this.moduleTestEnvironment); + if (this.testEnvironmentArg) { + extend(this.testEnvironment, this.testEnvironmentArg); + } + + QUnit.testStart( { + name: this.testName + } ); + + // allow utility functions to access the current test environment + // TODO why?? + QUnit.current_testEnvironment = this.testEnvironment; + + try { + if ( !config.pollution ) { + saveGlobal(); + } + + this.testEnvironment.setup.call(this.testEnvironment); + } catch(e) { + QUnit.ok( false, "Setup failed on " + this.testName + ": " + e.message ); + } + }, + run: function() { + if ( this.async ) { + QUnit.stop(); + } + + if ( config.notrycatch ) { + this.callback.call(this.testEnvironment); + return; + } + try { + this.callback.call(this.testEnvironment); + } catch(e) { + fail("Test " + this.testName + " died, exception and test follows", e, this.callback); + QUnit.ok( false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e) ); + // else next test will carry the responsibility + saveGlobal(); + + // Restart the tests if they're blocking + if ( config.blocking ) { + start(); + } + } + }, + teardown: function() { + try { + this.testEnvironment.teardown.call(this.testEnvironment); + checkPollution(); + } catch(e) { + QUnit.ok( false, "Teardown failed on " + this.testName + ": " + e.message ); + } + }, + finish: function() { + if ( this.expected && this.expected != this.assertions.length ) { + QUnit.ok( false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run" ); + } + + var good = 0, bad = 0, + tests = id("qunit-tests"); + + config.stats.all += this.assertions.length; + config.moduleStats.all += this.assertions.length; + + if ( tests ) { + var ol = document.createElement("ol"); + + for ( var i = 0; i < this.assertions.length; i++ ) { + var assertion = this.assertions[i]; + + var li = document.createElement("li"); + li.className = assertion.result ? "pass" : "fail"; + li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); + ol.appendChild( li ); + + if ( assertion.result ) { + good++; + } else { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + + // store result when possible + if ( QUnit.config.reorder && defined.sessionStorage ) { + if (bad) { + sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); + } else { + sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); + } + } + + if (bad == 0) { + ol.style.display = "none"; + } + + var b = document.createElement("strong"); + b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; + + var a = document.createElement("a"); + a.innerHTML = "Rerun"; + a.href = QUnit.url({ filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); + + addEvent(b, "click", function() { + var next = b.nextSibling.nextSibling, + display = next.style.display; + next.style.display = display === "none" ? "block" : "none"; + }); + + addEvent(b, "dblclick", function(e) { + var target = e && e.target ? e.target : window.event.srcElement; + if ( target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b" ) { + target = target.parentNode; + } + if ( window.location && target.nodeName.toLowerCase() === "strong" ) { + window.location = QUnit.url({ filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") }); + } + }); + + var li = id(this.id); + li.className = bad ? "fail" : "pass"; + li.removeChild( li.firstChild ); + li.appendChild( b ); + li.appendChild( a ); + li.appendChild( ol ); + + } else { + for ( var i = 0; i < this.assertions.length; i++ ) { + if ( !this.assertions[i].result ) { + bad++; + config.stats.bad++; + config.moduleStats.bad++; + } + } + } + + try { + QUnit.reset(); + } catch(e) { + fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); + } + + QUnit.testDone( { + name: this.testName, + failed: bad, + passed: this.assertions.length - bad, + total: this.assertions.length + } ); + }, + + queue: function() { + var test = this; + synchronize(function() { + test.init(); + }); + function run() { + // each of these can by async + synchronize(function() { + test.setup(); + }); + synchronize(function() { + test.run(); + }); + synchronize(function() { + test.teardown(); + }); + synchronize(function() { + test.finish(); + }); + } + // defer when previous test run passed, if storage is available + var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); + if (bad) { + run(); + } else { + synchronize(run); + }; + } + +}; + +var QUnit = { + + // call on start of module test to prepend name to all tests + module: function(name, testEnvironment) { + config.currentModule = name; + config.currentModuleTestEnviroment = testEnvironment; + }, + + asyncTest: function(testName, expected, callback) { + if ( arguments.length === 2 ) { + callback = expected; + expected = 0; + } + + QUnit.test(testName, expected, callback, true); + }, + + test: function(testName, expected, callback, async) { + var name = '' + testName + '', testEnvironmentArg; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + // is 2nd argument a testEnvironment? + if ( expected && typeof expected === 'object') { + testEnvironmentArg = expected; + expected = null; + } + + if ( config.currentModule ) { + name = '' + config.currentModule + ": " + name; + } + + if ( !validTest(config.currentModule + ": " + testName) ) { + return; + } + + var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); + test.module = config.currentModule; + test.moduleTestEnvironment = config.currentModuleTestEnviroment; + test.queue(); + }, + + /** + * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. + */ + expect: function(asserts) { + config.current.expected = asserts; + }, + + /** + * Asserts true. + * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); + */ + ok: function(a, msg) { + a = !!a; + var details = { + result: a, + message: msg + }; + msg = escapeHtml(msg); + QUnit.log(details); + config.current.assertions.push({ + result: a, + message: msg + }); + }, + + /** + * Checks that the first two arguments are equal, with an optional message. + * Prints out both actual and expected values. + * + * Prefered to ok( actual == expected, message ) + * + * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); + * + * @param Object actual + * @param Object expected + * @param String message (optional) + */ + equal: function(actual, expected, message) { + QUnit.push(expected == actual, actual, expected, message); + }, + + notEqual: function(actual, expected, message) { + QUnit.push(expected != actual, actual, expected, message); + }, + + deepEqual: function(actual, expected, message) { + QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); + }, + + notDeepEqual: function(actual, expected, message) { + QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); + }, + + strictEqual: function(actual, expected, message) { + QUnit.push(expected === actual, actual, expected, message); + }, + + notStrictEqual: function(actual, expected, message) { + QUnit.push(expected !== actual, actual, expected, message); + }, + + raises: function(block, expected, message) { + var actual, ok = false; + + if (typeof expected === 'string') { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + if (actual) { + // we don't want to validate thrown error + if (!expected) { + ok = true; + // expected is a regexp + } else if (QUnit.objectType(expected) === "regexp") { + ok = expected.test(actual); + // expected is a constructor + } else if (actual instanceof expected) { + ok = true; + // expected is a validation function which returns true is validation passed + } else if (expected.call({}, actual) === true) { + ok = true; + } + } + + QUnit.ok(ok, message); + }, + + start: function() { + config.semaphore--; + if (config.semaphore > 0) { + // don't start until equal number of stop-calls + return; + } + if (config.semaphore < 0) { + // ignore if start is called more often then stop + config.semaphore = 0; + } + // A slight delay, to avoid any current callbacks + if ( defined.setTimeout ) { + window.setTimeout(function() { + if ( config.timeout ) { + clearTimeout(config.timeout); + } + + config.blocking = false; + process(); + }, 13); + } else { + config.blocking = false; + process(); + } + }, + + stop: function(timeout) { + config.semaphore++; + config.blocking = true; + + if ( timeout && defined.setTimeout ) { + clearTimeout(config.timeout); + config.timeout = window.setTimeout(function() { + QUnit.ok( false, "Test timed out" ); + QUnit.start(); + }, timeout); + } + } +}; + +// Backwards compatibility, deprecated +QUnit.equals = QUnit.equal; +QUnit.same = QUnit.deepEqual; + +// Maintain internal state +var config = { + // The queue of tests to run + queue: [], + + // block until document ready + blocking: true, + + // by default, run previously failed tests first + // very useful in combination with "Hide passed tests" checked + reorder: true, + + noglobals: false, + notrycatch: false +}; + +// Load paramaters +(function() { + var location = window.location || { search: "", protocol: "file:" }, + params = location.search.slice( 1 ).split( "&" ), + length = params.length, + urlParams = {}, + current; + + if ( params[ 0 ] ) { + for ( var i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + urlParams[ current[ 0 ] ] = current[ 1 ]; + if ( current[ 0 ] in config ) { + config[ current[ 0 ] ] = current[ 1 ]; + } + } + } + + QUnit.urlParams = urlParams; + config.filter = urlParams.filter; + + // Figure out if we're running the tests from a server or not + QUnit.isLocal = !!(location.protocol === 'file:'); +})(); + +// Expose the API as global variables, unless an 'exports' +// object exists, in that case we assume we're in CommonJS +if ( typeof exports === "undefined" || typeof require === "undefined" ) { + extend(window, QUnit); + window.QUnit = QUnit; +} else { + extend(exports, QUnit); + exports.QUnit = QUnit; +} + +// define these after exposing globals to keep them in these QUnit namespace only +extend(QUnit, { + config: config, + + // Initialize the configuration options + init: function() { + extend(config, { + stats: { all: 0, bad: 0 }, + moduleStats: { all: 0, bad: 0 }, + started: +new Date, + updateRate: 1000, + blocking: false, + autostart: true, + autorun: false, + filter: "", + queue: [], + semaphore: 0 + }); + + var tests = id( "qunit-tests" ), + banner = id( "qunit-banner" ), + result = id( "qunit-testresult" ); + + if ( tests ) { + tests.innerHTML = ""; + } + + if ( banner ) { + banner.className = ""; + } + + if ( result ) { + result.parentNode.removeChild( result ); + } + + if ( tests ) { + result = document.createElement( "p" ); + result.id = "qunit-testresult"; + result.className = "result"; + tests.parentNode.insertBefore( result, tests ); + result.innerHTML = 'Running...
     '; + } + }, + + /** + * Resets the test setup. Useful for tests that modify the DOM. + * + * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. + */ + reset: function() { + if ( window.jQuery ) { + jQuery( "#qunit-fixture" ).html( config.fixture ); + } else { + var main = id( 'qunit-fixture' ); + if ( main ) { + main.innerHTML = config.fixture; + } + } + }, + + /** + * Trigger an event on an element. + * + * @example triggerEvent( document.body, "click" ); + * + * @param DOMElement elem + * @param String type + */ + triggerEvent: function( elem, type, event ) { + if ( document.createEvent ) { + event = document.createEvent("MouseEvents"); + event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + elem.dispatchEvent( event ); + + } else if ( elem.fireEvent ) { + elem.fireEvent("on"+type); + } + }, + + // Safe object type checking + is: function( type, obj ) { + return QUnit.objectType( obj ) == type; + }, + + objectType: function( obj ) { + if (typeof obj === "undefined") { + return "undefined"; + + // consider: typeof null === object + } + if (obj === null) { + return "null"; + } + + var type = Object.prototype.toString.call( obj ) + .match(/^\[object\s(.*)\]$/)[1] || ''; + + switch (type) { + case 'Number': + if (isNaN(obj)) { + return "nan"; + } else { + return "number"; + } + case 'String': + case 'Boolean': + case 'Array': + case 'Date': + case 'RegExp': + case 'Function': + return type.toLowerCase(); + } + if (typeof obj === "object") { + return "object"; + } + return undefined; + }, + + push: function(result, actual, expected, message) { + var details = { + result: result, + message: message, + actual: actual, + expected: expected + }; + + message = escapeHtml(message) || (result ? "okay" : "failed"); + message = '' + message + ""; + expected = escapeHtml(QUnit.jsDump.parse(expected)); + actual = escapeHtml(QUnit.jsDump.parse(actual)); + var output = message + ''; + if (actual != expected) { + output += ''; + output += ''; + } + if (!result) { + var source = sourceFromStacktrace(); + if (source) { + details.source = source; + output += ''; + } + } + output += "
    Expected:
    ' + expected + '
    Result:
    ' + actual + '
    Diff:
    ' + QUnit.diff(expected, actual) +'
    Source:
    ' + escapeHtml(source) + '
    "; + + QUnit.log(details); + + config.current.assertions.push({ + result: !!result, + message: output + }); + }, + + url: function( params ) { + params = extend( extend( {}, QUnit.urlParams ), params ); + var querystring = "?", + key; + for ( key in params ) { + querystring += encodeURIComponent( key ) + "=" + + encodeURIComponent( params[ key ] ) + "&"; + } + return window.location.pathname + querystring.slice( 0, -1 ); + }, + + // Logging callbacks; all receive a single argument with the listed properties + // run test/logs.html for any related changes + begin: function() {}, + // done: { failed, passed, total, runtime } + done: function() {}, + // log: { result, actual, expected, message } + log: function() {}, + // testStart: { name } + testStart: function() {}, + // testDone: { name, failed, passed, total } + testDone: function() {}, + // moduleStart: { name } + moduleStart: function() {}, + // moduleDone: { name, failed, passed, total } + moduleDone: function() {} +}); + +if ( typeof document === "undefined" || document.readyState === "complete" ) { + config.autorun = true; +} + +addEvent(window, "load", function() { + QUnit.begin({}); + + // Initialize the config, saving the execution queue + var oldconfig = extend({}, config); + QUnit.init(); + extend(config, oldconfig); + + config.blocking = false; + + var userAgent = id("qunit-userAgent"); + if ( userAgent ) { + userAgent.innerHTML = navigator.userAgent; + } + var banner = id("qunit-header"); + if ( banner ) { + banner.innerHTML = ' ' + banner.innerHTML + ' ' + + '' + + ''; + addEvent( banner, "change", function( event ) { + var params = {}; + params[ event.target.name ] = event.target.checked ? true : undefined; + window.location = QUnit.url( params ); + }); + } + + var toolbar = id("qunit-testrunner-toolbar"); + if ( toolbar ) { + var filter = document.createElement("input"); + filter.type = "checkbox"; + filter.id = "qunit-filter-pass"; + addEvent( filter, "click", function() { + var ol = document.getElementById("qunit-tests"); + if ( filter.checked ) { + ol.className = ol.className + " hidepass"; + } else { + var tmp = " " + ol.className.replace( /[\n\t\r]/g, " " ) + " "; + ol.className = tmp.replace(/ hidepass /, " "); + } + if ( defined.sessionStorage ) { + if (filter.checked) { + sessionStorage.setItem("qunit-filter-passed-tests", "true"); + } else { + sessionStorage.removeItem("qunit-filter-passed-tests"); + } + } + }); + if ( defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests") ) { + filter.checked = true; + var ol = document.getElementById("qunit-tests"); + ol.className = ol.className + " hidepass"; + } + toolbar.appendChild( filter ); + + var label = document.createElement("label"); + label.setAttribute("for", "qunit-filter-pass"); + label.innerHTML = "Hide passed tests"; + toolbar.appendChild( label ); + } + + var main = id('qunit-fixture'); + if ( main ) { + config.fixture = main.innerHTML; + } + + if (config.autostart) { + QUnit.start(); + } +}); + +function done() { + config.autorun = true; + + // Log the last module results + if ( config.currentModule ) { + QUnit.moduleDone( { + name: config.currentModule, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all + } ); + } + + var banner = id("qunit-banner"), + tests = id("qunit-tests"), + runtime = +new Date - config.started, + passed = config.stats.all - config.stats.bad, + html = [ + 'Tests completed in ', + runtime, + ' milliseconds.
    ', + '', + passed, + ' tests of ', + config.stats.all, + ' passed, ', + config.stats.bad, + ' failed.' + ].join(''); + + if ( banner ) { + banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); + } + + if ( tests ) { + id( "qunit-testresult" ).innerHTML = html; + } + + if ( typeof document !== "undefined" && document.title ) { + // show ✖ for good, ✔ for bad suite result in title + // use escape sequences in case file gets loaded with non-utf-8-charset + document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; + } + + QUnit.done( { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + } ); +} + +function validTest( name ) { + var filter = config.filter, + run = false; + + if ( !filter ) { + return true; + } + + var not = filter.charAt( 0 ) === "!"; + if ( not ) { + filter = filter.slice( 1 ); + } + + if ( name.indexOf( filter ) !== -1 ) { + return !not; + } + + if ( not ) { + run = true; + } + + return run; +} + +// so far supports only Firefox, Chrome and Opera (buggy) +// could be extended in the future to use something like https://github.com/csnover/TraceKit +function sourceFromStacktrace() { + try { + throw new Error(); + } catch ( e ) { + if (e.stacktrace) { + // Opera + return e.stacktrace.split("\n")[6]; + } else if (e.stack) { + // Firefox, Chrome + return e.stack.split("\n")[4]; + } + } +} + +function escapeHtml(s) { + if (!s) { + return ""; + } + s = s + ""; + return s.replace(/[\&"<>\\]/g, function(s) { + switch(s) { + case "&": return "&"; + case "\\": return "\\\\"; + case '"': return '\"'; + case "<": return "<"; + case ">": return ">"; + default: return s; + } + }); +} + +function synchronize( callback ) { + config.queue.push( callback ); + + if ( config.autorun && !config.blocking ) { + process(); + } +} + +function process() { + var start = (new Date()).getTime(); + + while ( config.queue.length && !config.blocking ) { + if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) { + config.queue.shift()(); + } else { + window.setTimeout( process, 13 ); + break; + } + } + if (!config.blocking && !config.queue.length) { + done(); + } +} + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in window ) { + config.pollution.push( key ); + } + } +} + +function checkPollution( name ) { + var old = config.pollution; + saveGlobal(); + + var newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + ok( false, "Introduced global variable(s): " + newGlobals.join(", ") ); + } + + var deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") ); + } +} + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var result = a.slice(); + for ( var i = 0; i < result.length; i++ ) { + for ( var j = 0; j < b.length; j++ ) { + if ( result[i] === b[j] ) { + result.splice(i, 1); + i--; + break; + } + } + } + return result; +} + +function fail(message, exception, callback) { + if ( typeof console !== "undefined" && console.error && console.warn ) { + console.error(message); + console.error(exception); + console.warn(callback.toString()); + + } else if ( window.opera && opera.postError ) { + opera.postError(message, exception, callback.toString); + } +} + +function extend(a, b) { + for ( var prop in b ) { + if ( b[prop] === undefined ) { + delete a[prop]; + } else { + a[prop] = b[prop]; + } + } + + return a; +} + +function addEvent(elem, type, fn) { + if ( elem.addEventListener ) { + elem.addEventListener( type, fn, false ); + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, fn ); + } else { + fn(); + } +} + +function id(name) { + return !!(typeof document !== "undefined" && document && document.getElementById) && + document.getElementById( name ); +} + +// Test for equality any JavaScript type. +// Discussions and reference: http://philrathe.com/articles/equiv +// Test suites: http://philrathe.com/tests/equiv +// Author: Philippe Rathé +QUnit.equiv = function () { + + var innerEquiv; // the real equiv function + var callers = []; // stack to decide between skip/abort functions + var parents = []; // stack to avoiding loops from circular referencing + + // Call the o related callback with the given arguments. + function bindCallbacks(o, callbacks, args) { + var prop = QUnit.objectType(o); + if (prop) { + if (QUnit.objectType(callbacks[prop]) === "function") { + return callbacks[prop].apply(callbacks, args); + } else { + return callbacks[prop]; // or undefined + } + } + } + + var callbacks = function () { + + // for string, boolean, number and null + function useStrictEquality(b, a) { + if (b instanceof a.constructor || a instanceof b.constructor) { + // to catch short annotaion VS 'new' annotation of a declaration + // e.g. var i = 1; + // var j = new Number(1); + return a == b; + } else { + return a === b; + } + } + + return { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + + "nan": function (b) { + return isNaN(b); + }, + + "date": function (b, a) { + return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); + }, + + "regexp": function (b, a) { + return QUnit.objectType(b) === "regexp" && + a.source === b.source && // the regex itself + a.global === b.global && // and its modifers (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline; + }, + + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function () { + var caller = callers[callers.length - 1]; + return caller !== Object && + typeof caller !== "undefined"; + }, + + "array": function (b, a) { + var i, j, loop; + var len; + + // b could be an object literal here + if ( ! (QUnit.objectType(b) === "array")) { + return false; + } + + len = a.length; + if (len !== b.length) { // safe and faster + return false; + } + + //track reference to avoid circular references + parents.push(a); + for (i = 0; i < len; i++) { + loop = false; + for(j=0;j= 0) { + type = "array"; + } else { + type = typeof obj; + } + return type; + }, + separator:function() { + return this.multiline ? this.HTML ? '
    ' : '\n' : this.HTML ? ' ' : ' '; + }, + indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing + if ( !this.multiline ) + return ''; + var chr = this.indentChar; + if ( this.HTML ) + chr = chr.replace(/\t/g,' ').replace(/ /g,' '); + return Array( this._depth_ + (extra||0) ).join(chr); + }, + up:function( a ) { + this._depth_ += a || 1; + }, + down:function( a ) { + this._depth_ -= a || 1; + }, + setParser:function( name, parser ) { + this.parsers[name] = parser; + }, + // The next 3 are exposed so you can use them + quote:quote, + literal:literal, + join:join, + // + _depth_: 1, + // This is the list of parsers, to modify them, use jsDump.setParser + parsers:{ + window: '[Window]', + document: '[Document]', + error:'[ERROR]', //when no parser is found, shouldn't happen + unknown: '[Unknown]', + 'null':'null', + 'undefined':'undefined', + 'function':function( fn ) { + var ret = 'function', + name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE + if ( name ) + ret += ' ' + name; + ret += '('; + + ret = [ ret, QUnit.jsDump.parse( fn, 'functionArgs' ), '){'].join(''); + return join( ret, QUnit.jsDump.parse(fn,'functionCode'), '}' ); + }, + array: array, + nodelist: array, + arguments: array, + object:function( map ) { + var ret = [ ]; + QUnit.jsDump.up(); + for ( var key in map ) + ret.push( QUnit.jsDump.parse(key,'key') + ': ' + QUnit.jsDump.parse(map[key]) ); + QUnit.jsDump.down(); + return join( '{', ret, '}' ); + }, + node:function( node ) { + var open = QUnit.jsDump.HTML ? '<' : '<', + close = QUnit.jsDump.HTML ? '>' : '>'; + + var tag = node.nodeName.toLowerCase(), + ret = open + tag; + + for ( var a in QUnit.jsDump.DOMAttrs ) { + var val = node[QUnit.jsDump.DOMAttrs[a]]; + if ( val ) + ret += ' ' + a + '=' + QUnit.jsDump.parse( val, 'attribute' ); + } + return ret + close + open + '/' + tag + close; + }, + functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function + var l = fn.length; + if ( !l ) return ''; + + var args = Array(l); + while ( l-- ) + args[l] = String.fromCharCode(97+l);//97 is 'a' + return ' ' + args.join(', ') + ' '; + }, + key:quote, //object calls it internally, the key part of an item in a map + functionCode:'[code]', //function calls it internally, it's the content of the function + attribute:quote, //node calls it internally, it's an html attribute value + string:quote, + date:quote, + regexp:literal, //regex + number:literal, + 'boolean':literal + }, + DOMAttrs:{//attributes to dump from nodes, name=>realName + id:'id', + name:'name', + 'class':'className' + }, + HTML:false,//if true, entities are escaped ( <, >, \t, space and \n ) + indentChar:' ',//indentation unit + multiline:true //if true, items in a collection, are separated by a \n, else just a space. + }; + + return jsDump; +})(); + +// from Sizzle.js +function getText( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += getText( elem.childNodes ); + } + } + + return ret; +}; + +/* + * Javascript Diff Algorithm + * By John Resig (http://ejohn.org/) + * Modified by Chu Alan "sprite" + * + * Released under the MIT license. + * + * More Info: + * http://ejohn.org/projects/javascript-diff-algorithm/ + * + * Usage: QUnit.diff(expected, actual) + * + * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" + */ +QUnit.diff = (function() { + function diff(o, n){ + var ns = new Object(); + var os = new Object(); + + for (var i = 0; i < n.length; i++) { + if (ns[n[i]] == null) + ns[n[i]] = { + rows: new Array(), + o: null + }; + ns[n[i]].rows.push(i); + } + + for (var i = 0; i < o.length; i++) { + if (os[o[i]] == null) + os[o[i]] = { + rows: new Array(), + n: null + }; + os[o[i]].rows.push(i); + } + + for (var i in ns) { + if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { + n[ns[i].rows[0]] = { + text: n[ns[i].rows[0]], + row: os[i].rows[0] + }; + o[os[i].rows[0]] = { + text: o[os[i].rows[0]], + row: ns[i].rows[0] + }; + } + } + + for (var i = 0; i < n.length - 1; i++) { + if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && + n[i + 1] == o[n[i].row + 1]) { + n[i + 1] = { + text: n[i + 1], + row: n[i].row + 1 + }; + o[n[i].row + 1] = { + text: o[n[i].row + 1], + row: i + 1 + }; + } + } + + for (var i = n.length - 1; i > 0; i--) { + if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && + n[i - 1] == o[n[i].row - 1]) { + n[i - 1] = { + text: n[i - 1], + row: n[i].row - 1 + }; + o[n[i].row - 1] = { + text: o[n[i].row - 1], + row: i - 1 + }; + } + } + + return { + o: o, + n: n + }; + } + + return function(o, n){ + o = o.replace(/\s+$/, ''); + n = n.replace(/\s+$/, ''); + var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); + + var str = ""; + + var oSpace = o.match(/\s+/g); + if (oSpace == null) { + oSpace = [" "]; + } + else { + oSpace.push(" "); + } + var nSpace = n.match(/\s+/g); + if (nSpace == null) { + nSpace = [" "]; + } + else { + nSpace.push(" "); + } + + if (out.n.length == 0) { + for (var i = 0; i < out.o.length; i++) { + str += '' + out.o[i] + oSpace[i] + ""; + } + } + else { + if (out.n[0].text == null) { + for (n = 0; n < out.o.length && out.o[n].text == null; n++) { + str += '' + out.o[n] + oSpace[n] + ""; + } + } + + for (var i = 0; i < out.n.length; i++) { + if (out.n[i].text == null) { + str += '' + out.n[i] + nSpace[i] + ""; + } + else { + var pre = ""; + + for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { + pre += '' + out.o[n] + oSpace[n] + ""; + } + str += " " + out.n[i].text + nSpace[i] + pre; + } + } + } + + return str; + }; +})(); + +})(this); diff --git a/test/stress_phystoken.tok b/test/stress_phystoken.tok new file mode 100644 index 0000000..8d1b6be --- /dev/null +++ b/test/stress_phystoken.tok @@ -0,0 +1,52 @@ +# Here's some random Python so that test_tokenize_myself will have some +# stressful stuff to try. This file is .tok instead of .py so pylint won't +# complain about it, check_eol won't look at it, etc. + +first_back = """\ +hey there! +""" + +other_back = """ +hey \ +there +""" + +lots_of_back = """\ +hey \ +there +""" +# This next line is supposed to have trailing whitespace: +fake_back = """\ +ouch +""" + +# Lots of difficulty happens with code like: +# +# fake_back = """\ +# ouch +# """ +# +# Ugh, the edge cases... + +# What about a comment like this\ +"what's this string doing here?" + +class C(object): + def there(): + this = 5 + \ + 7 + that = \ + "a continued line" + +cont1 = "one line of text" + \ + "another line of text" + +a_long_string = \ + "part 1" \ + "2" \ + "3 is longer" + +def hello(): + print("Hello world!") + +hello() diff --git a/test/stress_phystoken_dos.tok b/test/stress_phystoken_dos.tok new file mode 100644 index 0000000..b08fd70 --- /dev/null +++ b/test/stress_phystoken_dos.tok @@ -0,0 +1,52 @@ +# Here's some random Python so that test_tokenize_myself will have some +# stressful stuff to try. This file is .tok instead of .py so pylint won't +# complain about it, check_eol won't look at it, etc. + +first_back = """\ +hey there! +""" + +other_back = """ +hey \ +there +""" + +lots_of_back = """\ +hey \ +there +""" +# This next line is supposed to have trailing whitespace: +fake_back = """\ +ouch +""" + +# Lots of difficulty happens with code like: +# +# fake_back = """\ +# ouch +# """ +# +# Ugh, the edge cases... + +# What about a comment like this\ +"what's this string doing here?" + +class C(object): + def there(): + this = 5 + \ + 7 + that = \ + "a continued line" + +cont1 = "one line of text" + \ + "another line of text" + +a_long_string = \ + "part 1" \ + "2" \ + "3 is longer" + +def hello(): + print("Hello world!") + +hello() diff --git a/test/test_api.py b/test/test_api.py new file mode 100644 index 0000000..559684b --- /dev/null +++ b/test/test_api.py @@ -0,0 +1,571 @@ +"""Tests for Coverage's api.""" + +import fnmatch, os, re, sys, textwrap + +import coverage +from coverage.backward import StringIO + +from test.coveragetest import CoverageTest + + +class SingletonApiTest(CoverageTest): + """Tests of the old-fashioned singleton API.""" + + def setUp(self): + super(SingletonApiTest, self).setUp() + # These tests use the singleton module interface. Prevent it from + # writing .coverage files at exit. + coverage.use_cache(0) + + def do_report_work(self, modname): + """Create a module named `modname`, then measure it.""" + coverage.erase() + + self.make_file(modname+".py", """\ + a = 1 + b = 2 + if b == 3: + c = 4 + d = 5 + e = 6 + f = 7 + """) + + # Import the python file, executing it. + self.start_import_stop(coverage, modname) + + def test_simple(self): + coverage.erase() + + self.make_file("mycode.py", """\ + a = 1 + b = 2 + if b == 3: + c = 4 + d = 5 + """) + + # Import the python file, executing it. + self.start_import_stop(coverage, "mycode") + + _, statements, missing, missingtext = coverage.analysis("mycode.py") + self.assertEqual(statements, [1,2,3,4,5]) + self.assertEqual(missing, [4]) + self.assertEqual(missingtext, "4") + + def test_report(self): + self.do_report_work("mycode2") + coverage.report(["mycode2.py"]) + self.assertEqual(self.stdout(), textwrap.dedent("""\ + Name Stmts Miss Cover Missing + --------------------------------------- + mycode2 7 3 57% 4-6 + """)) + + def test_report_file(self): + # The file= argument of coverage.report makes the report go there. + self.do_report_work("mycode3") + fout = StringIO() + coverage.report(["mycode3.py"], file=fout) + self.assertEqual(self.stdout(), "") + self.assertEqual(fout.getvalue(), textwrap.dedent("""\ + Name Stmts Miss Cover Missing + --------------------------------------- + mycode3 7 3 57% 4-6 + """)) + + def test_report_default(self): + # Calling report() with no morfs will report on whatever was executed. + self.do_report_work("mycode4") + coverage.report() + rpt = re.sub(r"\s+", " ", self.stdout()) + self.assertIn("mycode4 7 3 57% 4-6", rpt) + + +class ApiTest(CoverageTest): + """Api-oriented tests for Coverage.""" + + def clean_files(self, files, pats): + """Remove names matching `pats` from `files`, a list of filenames.""" + good = [] + for f in files: + for pat in pats: + if fnmatch.fnmatch(f, pat): + break + else: + good.append(f) + return good + + def assertFiles(self, files): + """Assert that the files here are `files`, ignoring the usual junk.""" + here = os.listdir(".") + here = self.clean_files(here, ["*.pyc", "__pycache__"]) + self.assertSameElements(here, files) + + def test_unexecuted_file(self): + cov = coverage.coverage() + + self.make_file("mycode.py", """\ + a = 1 + b = 2 + if b == 3: + c = 4 + d = 5 + """) + + self.make_file("not_run.py", """\ + fooey = 17 + """) + + # Import the python file, executing it. + self.start_import_stop(cov, "mycode") + + _, statements, missing, _ = cov.analysis("not_run.py") + self.assertEqual(statements, [1]) + self.assertEqual(missing, [1]) + + def test_filenames(self): + + self.make_file("mymain.py", """\ + import mymod + a = 1 + """) + + self.make_file("mymod.py", """\ + fooey = 17 + """) + + # Import the python file, executing it. + cov = coverage.coverage() + self.start_import_stop(cov, "mymain") + + filename, _, _, _ = cov.analysis("mymain.py") + self.assertEqual(os.path.basename(filename), "mymain.py") + filename, _, _, _ = cov.analysis("mymod.py") + self.assertEqual(os.path.basename(filename), "mymod.py") + + filename, _, _, _ = cov.analysis(sys.modules["mymain"]) + self.assertEqual(os.path.basename(filename), "mymain.py") + filename, _, _, _ = cov.analysis(sys.modules["mymod"]) + self.assertEqual(os.path.basename(filename), "mymod.py") + + # Import the python file, executing it again, once it's been compiled + # already. + cov = coverage.coverage() + self.start_import_stop(cov, "mymain") + + filename, _, _, _ = cov.analysis("mymain.py") + self.assertEqual(os.path.basename(filename), "mymain.py") + filename, _, _, _ = cov.analysis("mymod.py") + self.assertEqual(os.path.basename(filename), "mymod.py") + + filename, _, _, _ = cov.analysis(sys.modules["mymain"]) + self.assertEqual(os.path.basename(filename), "mymain.py") + filename, _, _, _ = cov.analysis(sys.modules["mymod"]) + self.assertEqual(os.path.basename(filename), "mymod.py") + + def test_ignore_stdlib(self): + self.make_file("mymain.py", """\ + import colorsys + a = 1 + hls = colorsys.rgb_to_hls(1.0, 0.5, 0.0) + """) + + # Measure without the stdlib. + cov1 = coverage.coverage() + self.assertEqual(cov1.config.cover_pylib, False) + self.start_import_stop(cov1, "mymain") + + # some statements were marked executed in mymain.py + _, statements, missing, _ = cov1.analysis("mymain.py") + self.assertNotEqual(statements, missing) + # but none were in colorsys.py + _, statements, missing, _ = cov1.analysis("colorsys.py") + self.assertEqual(statements, missing) + + # Measure with the stdlib. + cov2 = coverage.coverage(cover_pylib=True) + self.start_import_stop(cov2, "mymain") + + # some statements were marked executed in mymain.py + _, statements, missing, _ = cov2.analysis("mymain.py") + self.assertNotEqual(statements, missing) + # and some were marked executed in colorsys.py + _, statements, missing, _ = cov2.analysis("colorsys.py") + self.assertNotEqual(statements, missing) + + def test_include_can_measure_stdlib(self): + self.make_file("mymain.py", """\ + import colorsys, random + a = 1 + r, g, b = [random.random() for _ in range(3)] + hls = colorsys.rgb_to_hls(r, g, b) + """) + + # Measure without the stdlib, but include colorsys. + cov1 = coverage.coverage(cover_pylib=False, include=["*/colorsys.py"]) + self.start_import_stop(cov1, "mymain") + + # some statements were marked executed in colorsys.py + _, statements, missing, _ = cov1.analysis("colorsys.py") + self.assertNotEqual(statements, missing) + # but none were in random.py + _, statements, missing, _ = cov1.analysis("random.py") + self.assertEqual(statements, missing) + + def test_exclude_list(self): + cov = coverage.coverage() + cov.clear_exclude() + self.assertEqual(cov.get_exclude_list(), []) + cov.exclude("foo") + self.assertEqual(cov.get_exclude_list(), ["foo"]) + cov.exclude("bar") + self.assertEqual(cov.get_exclude_list(), ["foo", "bar"]) + self.assertEqual(cov._exclude_regex('exclude'), "(foo)|(bar)") + cov.clear_exclude() + self.assertEqual(cov.get_exclude_list(), []) + + def test_exclude_partial_list(self): + cov = coverage.coverage() + cov.clear_exclude(which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + cov.exclude("foo", which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), ["foo"]) + cov.exclude("bar", which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), ["foo", "bar"]) + self.assertEqual(cov._exclude_regex(which='partial'), "(foo)|(bar)") + cov.clear_exclude(which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + + def test_exclude_and_partial_are_separate_lists(self): + cov = coverage.coverage() + cov.clear_exclude(which='partial') + cov.clear_exclude(which='exclude') + cov.exclude("foo", which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), ['foo']) + self.assertEqual(cov.get_exclude_list(which='exclude'), []) + cov.exclude("bar", which='exclude') + self.assertEqual(cov.get_exclude_list(which='partial'), ['foo']) + self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar']) + cov.exclude("p2", which='partial') + cov.exclude("e2", which='exclude') + self.assertEqual(cov.get_exclude_list(which='partial'), ['foo', 'p2']) + self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar', 'e2']) + cov.clear_exclude(which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar', 'e2']) + cov.clear_exclude(which='exclude') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + self.assertEqual(cov.get_exclude_list(which='exclude'), []) + + def test_datafile_default(self): + # Default data file behavior: it's .coverage + self.make_file("datatest1.py", """\ + fooey = 17 + """) + + self.assertFiles(["datatest1.py"]) + cov = coverage.coverage() + self.start_import_stop(cov, "datatest1") + cov.save() + self.assertFiles(["datatest1.py", ".coverage"]) + + def test_datafile_specified(self): + # You can specify the data file name. + self.make_file("datatest2.py", """\ + fooey = 17 + """) + + self.assertFiles(["datatest2.py"]) + cov = coverage.coverage(data_file="cov.data") + self.start_import_stop(cov, "datatest2") + cov.save() + self.assertFiles(["datatest2.py", "cov.data"]) + + def test_datafile_and_suffix_specified(self): + # You can specify the data file name and suffix. + self.make_file("datatest3.py", """\ + fooey = 17 + """) + + self.assertFiles(["datatest3.py"]) + cov = coverage.coverage(data_file="cov.data", data_suffix="14") + self.start_import_stop(cov, "datatest3") + cov.save() + self.assertFiles(["datatest3.py", "cov.data.14"]) + + def test_datafile_from_rcfile(self): + # You can specify the data file name in the .coveragerc file + self.make_file("datatest4.py", """\ + fooey = 17 + """) + self.make_file(".coveragerc", """\ + [run] + data_file = mydata.dat + """) + + self.assertFiles(["datatest4.py", ".coveragerc"]) + cov = coverage.coverage() + self.start_import_stop(cov, "datatest4") + cov.save() + self.assertFiles(["datatest4.py", ".coveragerc", "mydata.dat"]) + + def test_empty_reporting(self): + # Used to be you'd get an exception reporting on nothing... + cov = coverage.coverage() + cov.erase() + cov.report() + + def test_start_stop_start_stop(self): + self.make_file("code1.py", """\ + code1 = 1 + """) + self.make_file("code2.py", """\ + code2 = 1 + code2 = 2 + """) + cov = coverage.coverage() + self.start_import_stop(cov, "code1") + cov.save() + self.start_import_stop(cov, "code2") + _, statements, missing, _ = cov.analysis("code1.py") + self.assertEqual(statements, [1]) + self.assertEqual(missing, []) + _, statements, missing, _ = cov.analysis("code2.py") + self.assertEqual(statements, [1, 2]) + self.assertEqual(missing, []) + + if 0: # expected failure + # for https://bitbucket.org/ned/coveragepy/issue/79 + def test_start_save_stop(self): + self.make_file("code1.py", """\ + code1 = 1 + """) + self.make_file("code2.py", """\ + code2 = 1 + code2 = 2 + """) + cov = coverage.coverage() + cov.start() + self.import_local_file("code1") + cov.save() + self.import_local_file("code2") + cov.stop() + + _, statements, missing, _ = cov.analysis("code1.py") + self.assertEqual(statements, [1]) + self.assertEqual(missing, []) + _, statements, missing, _ = cov.analysis("code2.py") + self.assertEqual(statements, [1, 2]) + self.assertEqual(missing, []) + + + +class UsingModulesMixin(object): + """A mixin for importing modules from test/modules and test/moremodules.""" + + run_in_temp_dir = False + + def setUp(self): + super(UsingModulesMixin, self).setUp() + # Parent class saves and restores sys.path, we can just modify it. + self.old_dir = os.getcwd() + os.chdir(self.nice_file(os.path.dirname(__file__), 'modules')) + sys.path.append(".") + sys.path.append("../moremodules") + + def tearDown(self): + os.chdir(self.old_dir) + super(UsingModulesMixin, self).tearDown() + + +class OmitIncludeTestsMixin(UsingModulesMixin): + """Test methods for coverage methods taking include and omit.""" + + def filenames_in(self, summary, filenames): + """Assert the `filenames` are in the keys of `summary`.""" + for filename in filenames.split(): + self.assertIn(filename, summary) + + def filenames_not_in(self, summary, filenames): + """Assert the `filenames` are not in the keys of `summary`.""" + for filename in filenames.split(): + self.assertNotIn(filename, summary) + + def test_nothing_specified(self): + result = self.coverage_usepkgs() + self.filenames_in(result, "p1a p1b p2a p2b othera otherb osa osb") + self.filenames_not_in(result, "p1c") + # Because there was no source= specified, we don't search for + # unexecuted files. + + def test_include(self): + result = self.coverage_usepkgs(include=["*/p1a.py"]) + self.filenames_in(result, "p1a") + self.filenames_not_in(result, "p1b p1c p2a p2b othera otherb osa osb") + + def test_include_2(self): + result = self.coverage_usepkgs(include=["*a.py"]) + self.filenames_in(result, "p1a p2a othera osa") + self.filenames_not_in(result, "p1b p1c p2b otherb osb") + + def test_include_as_string(self): + result = self.coverage_usepkgs(include="*a.py") + self.filenames_in(result, "p1a p2a othera osa") + self.filenames_not_in(result, "p1b p1c p2b otherb osb") + + def test_omit(self): + result = self.coverage_usepkgs(omit=["*/p1a.py"]) + self.filenames_in(result, "p1b p2a p2b") + self.filenames_not_in(result, "p1a p1c") + + def test_omit_2(self): + result = self.coverage_usepkgs(omit=["*a.py"]) + self.filenames_in(result, "p1b p2b otherb osb") + self.filenames_not_in(result, "p1a p1c p2a othera osa") + + def test_omit_as_string(self): + result = self.coverage_usepkgs(omit="*a.py") + self.filenames_in(result, "p1b p2b otherb osb") + self.filenames_not_in(result, "p1a p1c p2a othera osa") + + def test_omit_and_include(self): + result = self.coverage_usepkgs( include=["*/p1*"], omit=["*/p1a.py"]) + self.filenames_in(result, "p1b") + self.filenames_not_in(result, "p1a p1c p2a p2b") + + +class SourceOmitIncludeTest(OmitIncludeTestsMixin, CoverageTest): + """Test using `source`, `omit` and `include` when measuring code.""" + + def coverage_usepkgs(self, **kwargs): + """Run coverage on usepkgs and return the line summary. + + Arguments are passed to the `coverage.coverage` constructor. + + """ + cov = coverage.coverage(**kwargs) + cov.start() + import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + cov.stop() # pragma: nested + cov._harvest_data() # private! sshhh... + summary = cov.data.summary() + for k, v in list(summary.items()): + assert k.endswith(".py") + summary[k[:-3]] = v + return summary + + def test_source_package(self): + lines = self.coverage_usepkgs(source=["pkg1"]) + self.filenames_in(lines, "p1a p1b") + self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") + # Because source= was specified, we do search for unexecuted files. + self.assertEqual(lines['p1c'], 0) + + def test_source_package_dotted(self): + lines = self.coverage_usepkgs(source=["pkg1.p1b"]) + self.filenames_in(lines, "p1b") + self.filenames_not_in(lines, "p1a p1c p2a p2b othera otherb osa osb") + + +class ReportIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest): + """Tests of the report include/omit functionality.""" + + def coverage_usepkgs(self, **kwargs): + """Try coverage.report().""" + cov = coverage.coverage() + cov.start() + import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + cov.stop() # pragma: nested + report = StringIO() + cov.report(file=report, **kwargs) + return report.getvalue() + + +class XmlIncludeOmitTest(OmitIncludeTestsMixin, CoverageTest): + """Tests of the xml include/omit functionality. + + This also takes care of the HTML and annotate include/omit, by virtue + of the structure of the code. + + """ + + def coverage_usepkgs(self, **kwargs): + """Try coverage.xml_report().""" + cov = coverage.coverage() + cov.start() + import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + cov.stop() # pragma: nested + cov.xml_report(outfile="-", **kwargs) + return self.stdout() + + +class AnalysisTest(CoverageTest): + """Test the numerical analysis of results.""" + def test_many_missing_branches(self): + cov = coverage.coverage(branch=True) + + self.make_file("missing.py", """\ + def fun1(x): + if x == 1: + print("one") + else: + print("not one") + print("done") # pragma: nocover + + def fun2(x): + print("x") + + fun2(3) + """) + + # Import the python file, executing it. + self.start_import_stop(cov, "missing") + + nums = cov._analyze("missing.py").numbers + self.assertEqual(nums.n_files, 1) + self.assertEqual(nums.n_statements, 7) + self.assertEqual(nums.n_excluded, 1) + self.assertEqual(nums.n_missing, 3) + self.assertEqual(nums.n_branches, 2) + self.assertEqual(nums.n_partial_branches, 0) + self.assertEqual(nums.n_missing_branches, 2) + + +class PluginTest(CoverageTest): + """Test that the API works properly the way the plugins call it. + + We don't actually use the plugins, but these tests call the API the same + way they do. + + """ + def pretend_to_be_nose_with_cover(self, erase): + """This is what the nose --with-cover plugin does.""" + cov = coverage.coverage() + + self.make_file("no_biggie.py", """\ + a = 1 + b = 2 + if b == 1: + c = 4 + """) + + if erase: + cov.combine() + cov.erase() + cov.load() + self.start_import_stop(cov, "no_biggie") + cov.combine() + cov.save() + cov.report(["no_biggie.py"]) + self.assertEqual(self.stdout(), textwrap.dedent("""\ + Name Stmts Miss Cover Missing + ----------------------------------------- + no_biggie 4 1 75% 4 + """)) + + def test_nose_plugin(self): + self.pretend_to_be_nose_with_cover(erase=False) + + def test_nose_plugin_with_erase(self): + self.pretend_to_be_nose_with_cover(erase=True) diff --git a/test/test_arcs.py b/test/test_arcs.py new file mode 100644 index 0000000..fc16c0f --- /dev/null +++ b/test/test_arcs.py @@ -0,0 +1,571 @@ +"""Tests for Coverage.py's arc measurement.""" + +import sys +from test.coveragetest import CoverageTest + + +class SimpleArcTest(CoverageTest): + """Tests for Coverage.py's arc measurement.""" + + def test_simple_sequence(self): + self.check_coverage("""\ + a = 1 + b = 2 + """, + arcz=".1 12 2.") + self.check_coverage("""\ + a = 1 + + b = 3 + """, + arcz=".1 13 3.") + self.check_coverage("""\ + + a = 2 + b = 3 + + c = 5 + """, + arcz=".2 23 35 5-2") + + def test_function_def(self): + self.check_coverage("""\ + def foo(): + a = 2 + + foo() + """, + arcz=".1 .2 14 2. 4.") + + def test_if(self): + self.check_coverage("""\ + a = 1 + if len([]) == 0: + a = 3 + assert a == 3 + """, + arcz=".1 12 23 24 34 4.", arcz_missing="24") + self.check_coverage("""\ + a = 1 + if len([]) == 1: + a = 3 + assert a == 1 + """, + arcz=".1 12 23 24 34 4.", arcz_missing="23 34") + + def test_if_else(self): + self.check_coverage("""\ + if len([]) == 0: + a = 2 + else: + a = 4 + assert a == 2 + """, + arcz=".1 12 25 14 45 5.", arcz_missing="14 45") + self.check_coverage("""\ + if len([]) == 1: + a = 2 + else: + a = 4 + assert a == 4 + """, + arcz=".1 12 25 14 45 5.", arcz_missing="12 25") + + def test_compact_if(self): + self.check_coverage("""\ + a = 1 + if len([]) == 0: a = 2 + assert a == 2 + """, + arcz=".1 12 23 3.", arcz_missing="") + self.check_coverage("""\ + def fn(x): + if x % 2: return True + return False + a = fn(1) + assert a == True + """, + arcz=".1 14 45 5. .2 2. 23 3.", arcz_missing="23 3.") + + def test_multiline(self): + # The firstlineno of the a assignment below differs among Python + # versions. + if sys.version_info >= (2, 5): + arcz = ".1 15 5-2" + else: + arcz = ".1 15 5-1" + self.check_coverage("""\ + a = ( + 2 + + 3 + ) + b = \\ + 6 + """, + arcz=arcz, arcz_missing="") + + def test_if_return(self): + self.check_coverage("""\ + def if_ret(a): + if a: + return 3 + b = 4 + return 5 + x = if_ret(0) + if_ret(1) + assert x == 8 + """, + arcz=".1 16 67 7. .2 23 24 3. 45 5.", arcz_missing="" + ) + + def test_dont_confuse_exit_and_else(self): + self.check_coverage("""\ + def foo(): + if foo: + a = 3 + else: + a = 5 + return a + assert foo() == 3 # 7 + """, + arcz=".1 17 7. .2 23 36 25 56 6.", arcz_missing="25 56" + ) + self.check_coverage("""\ + def foo(): + if foo: + a = 3 + else: + a = 5 + foo() # 6 + """, + arcz=".1 16 6. .2 23 3. 25 5.", arcz_missing="25 5." + ) + + if 0: # expected failure + def test_lambdas_are_confusing_bug_90(self): + self.check_coverage("""\ + fn = lambda x: x + a = 1 + """, + arcz=".1 12 2." + ) + + +if sys.version_info >= (2, 6): + class WithTest(CoverageTest): + """Arc-measuring tests involving context managers.""" + + def test_with(self): + self.check_coverage("""\ + def example(): + with open("test", "w") as f: # exit + f.write("") + return 1 + + example() + """, + arcz=".1 .2 23 34 4. 16 6." + ) + + +class LoopArcTest(CoverageTest): + """Arc-measuring tests involving loops.""" + + def test_loop(self): + self.check_coverage("""\ + for i in range(10): + a = i + assert a == 9 + """, + arcz=".1 12 21 13 3.", arcz_missing="") + self.check_coverage("""\ + a = -1 + for i in range(0): + a = i + assert a == -1 + """, + arcz=".1 12 23 32 24 4.", arcz_missing="23 32") + + def test_nested_loop(self): + self.check_coverage("""\ + for i in range(3): + for j in range(3): + a = i + j + assert a == 4 + """, + arcz=".1 12 23 32 21 14 4.", arcz_missing="") + + def test_break(self): + self.check_coverage("""\ + for i in range(10): + a = i + break # 3 + a = 99 + assert a == 0 # 5 + """, + arcz=".1 12 23 35 15 41 5.", arcz_missing="15 41") + + def test_continue(self): + self.check_coverage("""\ + for i in range(10): + a = i + continue # 3 + a = 99 + assert a == 9 # 5 + """, + arcz=".1 12 23 31 15 41 5.", arcz_missing="41") + + def test_nested_breaks(self): + self.check_coverage("""\ + for i in range(3): + for j in range(3): + a = i + j + break # 4 + if i == 2: + break + assert a == 2 and i == 2 # 7 + """, + arcz=".1 12 23 34 45 25 56 51 67 17 7.", arcz_missing="17 25") + + def test_while_true(self): + # With "while 1", the loop knows it's constant. + self.check_coverage("""\ + a, i = 1, 0 + while 1: + if i >= 3: + a = 4 + break + i += 1 + assert a == 4 and i == 3 + """, + arcz=".1 12 23 34 45 36 63 57 7.", + ) + # With "while True", 2.x thinks it's computation, 3.x thinks it's + # constant. + if sys.version_info >= (3, 0): + arcz = ".1 12 23 34 45 36 63 57 7." + else: + arcz = ".1 12 23 27 34 45 36 62 57 7." + self.check_coverage("""\ + a, i = 1, 0 + while True: + if i >= 3: + a = 4 + break + i += 1 + assert a == 4 and i == 3 + """, + arcz=arcz, + ) + + def test_for_if_else_for(self): + self.check_coverage("""\ + def branches_2(l): + if l: + for e in l: + a = 4 + else: + a = 6 + + def branches_3(l): + for x in l: + if x: + for e in l: + a = 12 + else: + a = 14 + + branches_2([0,1]) + branches_3([0,1]) + """, + arcz= + ".1 18 8G GH H. " + ".2 23 34 43 26 3. 6. " + ".9 9A 9-8 AB BC CB B9 AE E9", + arcz_missing="26 6." + ) + + def test_for_else(self): + self.check_coverage("""\ + def forelse(seq): + for n in seq: + if n > 5: + break + else: + print('None of the values were greater than 5') + print('Done') + forelse([1,2]) + forelse([1,6]) + """, + arcz=".1 .2 23 32 34 47 26 67 7. 18 89 9." + ) + + if 0: # expected failure + def test_confusing_for_loop_bug_175(self): + self.check_coverage("""\ + o = [(1,2), (3,4)] + o = [a for a in o if a[0] > 1] + for tup in o: + x = tup[0] + y = tup[1] + """, + arcz=".1 12 23 34 45 53 3.", + arcz_missing="", arcz_unpredicted="") + self.check_coverage("""\ + o = [(1,2), (3,4)] + for tup in [a for a in o if a[0] > 1]: + x = tup[0] + y = tup[1] + """, + arcz=".1 12 23 34 42 2.", + arcz_missing="", arcz_unpredicted="") + + +class ExceptionArcTest(CoverageTest): + """Arc-measuring tests involving exception handling.""" + + def test_try_except(self): + self.check_coverage("""\ + a, b = 1, 1 + try: + a = 3 + except: + b = 5 + assert a == 3 and b == 1 + """, + arcz=".1 12 23 36 45 56 6.", arcz_missing="45 56") + self.check_coverage("""\ + a, b = 1, 1 + try: + a = 3 + raise Exception("Yikes!") + a = 5 + except: + b = 7 + assert a == 3 and b == 7 + """, + arcz=".1 12 23 34 58 67 78 8.", + arcz_missing="58", arcz_unpredicted="46") + + def test_hidden_raise(self): + self.check_coverage("""\ + a, b = 1, 1 + def oops(x): + if x % 2: raise Exception("odd") + try: + a = 5 + oops(1) + a = 7 + except: + b = 9 + assert a == 5 and b == 9 + """, + arcz=".1 12 .3 3-2 24 45 56 67 7A 89 9A A.", + arcz_missing="67 7A", arcz_unpredicted="68") + + def test_except_with_type(self): + self.check_coverage("""\ + a, b = 1, 1 + def oops(x): + if x % 2: raise ValueError("odd") + def try_it(x): + try: + a = 6 + oops(x) + a = 8 + except ValueError: + b = 10 + return a + assert try_it(0) == 8 # C + assert try_it(1) == 6 # D + """, + arcz=".1 12 .3 3-2 24 4C CD D. .5 56 67 78 8B 9A AB B-4", + arcz_missing="", + arcz_unpredicted="79") + + def test_try_finally(self): + self.check_coverage("""\ + a, c = 1, 1 + try: + a = 3 + finally: + c = 5 + assert a == 3 and c == 5 + """, + arcz=".1 12 23 35 56 6.", arcz_missing="") + self.check_coverage("""\ + a, c, d = 1, 1, 1 + try: + try: + a = 4 + finally: + c = 6 + except: + d = 8 + assert a == 4 and c == 6 and d == 1 # 9 + """, + arcz=".1 12 23 34 46 67 78 89 69 9.", + arcz_missing="67 78 89", arcz_unpredicted="") + self.check_coverage("""\ + a, c, d = 1, 1, 1 + try: + try: + a = 4 + raise Exception("Yikes!") + a = 6 + finally: + c = 8 + except: + d = 10 # A + assert a == 4 and c == 8 and d == 10 # B + """, + arcz=".1 12 23 34 45 68 89 8B 9A AB B.", + arcz_missing="68 8B", arcz_unpredicted="58") + + def test_finally_in_loop(self): + self.check_coverage("""\ + a, c, d, i = 1, 1, 1, 99 + try: + for i in range(5): + try: + a = 5 + if i > 0: + raise Exception("Yikes!") + a = 8 + finally: + c = 10 + except: + d = 12 # C + assert a == 5 and c == 10 and d == 12 # D + """, + arcz=".1 12 23 34 3D 45 56 67 68 8A A3 AB AD BC CD D.", + arcz_missing="3D AD", arcz_unpredicted="7A") + self.check_coverage("""\ + a, c, d, i = 1, 1, 1, 99 + try: + for i in range(5): + try: + a = 5 + if i > 10: + raise Exception("Yikes!") + a = 8 + finally: + c = 10 + except: + d = 12 # C + assert a == 8 and c == 10 and d == 1 # D + """, + arcz=".1 12 23 34 3D 45 56 67 68 8A A3 AB AD BC CD D.", + arcz_missing="67 AB AD BC CD", arcz_unpredicted="") + + + def test_break_in_finally(self): + self.check_coverage("""\ + a, c, d, i = 1, 1, 1, 99 + try: + for i in range(5): + try: + a = 5 + if i > 0: + break + a = 8 + finally: + c = 10 + except: + d = 12 # C + assert a == 5 and c == 10 and d == 1 # D + """, + arcz=".1 12 23 34 3D 45 56 67 68 7A 8A A3 AB AD BC CD D.", + arcz_missing="3D AB BC CD", arcz_unpredicted="") + + if 0: # expected failure + def test_finally_in_loop_2(self): + self.check_coverage("""\ + for i in range(5): + try: + j = 3 + finally: + f = 5 + g = 6 + h = 7 + """, + arcz=".1 12 23 35 56 61 17 7.", + arcz_missing="", arcz_unpredicted="") + + if sys.version_info >= (2, 5): + # Try-except-finally was new in 2.5 + def test_except_finally(self): + self.check_coverage("""\ + a, b, c = 1, 1, 1 + try: + a = 3 + except: + b = 5 + finally: + c = 7 + assert a == 3 and b == 1 and c == 7 + """, + arcz=".1 12 23 45 37 57 78 8.", arcz_missing="45 57") + self.check_coverage("""\ + a, b, c = 1, 1, 1 + def oops(x): + if x % 2: raise Exception("odd") + try: + a = 5 + oops(1) + a = 7 + except: + b = 9 + finally: + c = 11 + assert a == 5 and b == 9 and c == 11 + """, + arcz=".1 12 .3 3-2 24 45 56 67 7B 89 9B BC C.", + arcz_missing="67 7B", arcz_unpredicted="68") + + +class MiscArcTest(CoverageTest): + """Miscellaneous arc-measuring tests.""" + + def test_dict_literal(self): + self.check_coverage("""\ + d = { + 'a': 2, + 'b': 3, + 'c': { + 'd': 5, + 'e': 6, + } + } + assert d + """, + arcz=".1 19 9.") + + +class ExcludeTest(CoverageTest): + """Tests of exclusions to indicate known partial branches.""" + + def test_default(self): + # A number of forms of pragma comment are accepted. + self.check_coverage("""\ + a = 1 + if a: #pragma: no branch + b = 3 + c = 4 + if c: # pragma NOBRANCH + d = 6 + e = 7 + """, + [1,2,3,4,5,6,7], + arcz=".1 12 23 24 34 45 56 57 67 7.", arcz_missing="") + + def test_custom_pragmas(self): + self.check_coverage("""\ + a = 1 + while a: # [only some] + c = 3 + break + assert c == 5-2 + """, + [1,2,3,4,5], + partials=["only some"], + arcz=".1 12 23 34 45 25 5.", arcz_missing="") diff --git a/test/test_cmdline.py b/test/test_cmdline.py new file mode 100644 index 0000000..33f9021 --- /dev/null +++ b/test/test_cmdline.py @@ -0,0 +1,702 @@ +"""Test cmdline.py for coverage.""" + +import pprint, re, shlex, sys, textwrap +import mock +import coverage +import coverage.cmdline +from coverage.misc import ExceptionDuringRun + +from test.coveragetest import CoverageTest, OK, ERR + + +class CmdLineTest(CoverageTest): + """Tests of execution paths through the command line interpreter.""" + + run_in_temp_dir = False + + INIT_LOAD = """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .load()\n""" + + def model_object(self): + """Return a Mock suitable for use in CoverageScript.""" + mk = mock.Mock() + mk.coverage.return_value = mk + return mk + + def mock_command_line(self, args): + """Run `args` through the command line, with a Mock. + + Returns the Mock it used and the status code returned. + + """ + m = self.model_object() + ret = coverage.CoverageScript( + _covpkg=m, _run_python_file=m.run_python_file, + _run_python_module=m.run_python_module, _help_fn=m.help_fn + ).command_line(shlex.split(args)) + return m, ret + + def cmd_executes(self, args, code, ret=OK): + """Assert that the `args` end up executing the sequence in `code`.""" + m1, r1 = self.mock_command_line(args) + self.assertEqual(r1, ret, + "Wrong status: got %s, wanted %s" % (r1, ret) + ) + + # Remove all indentation, and change ".foo()" to "m2.foo()". + code = re.sub(r"(?m)^\s+", "", code) + code = re.sub(r"(?m)^\.", "m2.", code) + m2 = self.model_object() + code_obj = compile(code, "", "exec") + eval(code_obj, globals(), { 'm2': m2 }) + self.assert_same_method_calls(m1, m2) + + def cmd_executes_same(self, args1, args2): + """Assert that the `args1` executes the same as `args2`.""" + m1, r1 = self.mock_command_line(args1) + m2, r2 = self.mock_command_line(args2) + self.assertEqual(r1, r2) + self.assert_same_method_calls(m1, m2) + + def assert_same_method_calls(self, m1, m2): + """Assert that `m1.method_calls` and `m2.method_calls` are the same.""" + # Use a real equality comparison, but if it fails, use a nicer assert + # so we can tell what's going on. We have to use the real == first due + # to CmdOptionParser.__eq__ + if m1.method_calls != m2.method_calls: + pp1 = pprint.pformat(m1.method_calls) + pp2 = pprint.pformat(m2.method_calls) + self.assertMultiLineEqual(pp1+'\n', pp2+'\n') + + def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): + """Run a command line, and check that it prints the right help. + + Only the last function call in the mock is checked, which should be the + help message that we want to see. + + """ + m, r = self.mock_command_line(args) + self.assertEqual(r, ret, + "Wrong status: got %s, wanted %s" % (r, ret) + ) + if help_msg: + self.assertEqual(m.method_calls[-1], + ('help_fn', (help_msg,), {}) + ) + else: + self.assertEqual(m.method_calls[-1], + ('help_fn', (), {'topic':topic}) + ) + + +class CmdLineTestTest(CmdLineTest): + """Tests that our CmdLineTest helpers work.""" + def test_assert_same_method_calls(self): + # All the other tests here use self.cmd_executes_same in successful + # ways, so here we just check that it fails. + self.assertRaises(AssertionError, self.cmd_executes_same, "-e", "-c") + + +class ClassicCmdLineTest(CmdLineTest): + """Tests of the classic coverage.py command line.""" + + def test_erase(self): + # coverage -e + self.cmd_executes("-e", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .erase() + """) + self.cmd_executes_same("-e", "--erase") + + def test_execute(self): + # coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] + + # -x calls coverage.load first. + self.cmd_executes("-x foo.py", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .load() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + # -e -x calls coverage.erase first. + self.cmd_executes("-e -x foo.py", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + # --timid sets a flag, and program arguments get passed through. + self.cmd_executes("-x --timid foo.py abc 123", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=True, branch=None, config_file=True, source=None, include=None, omit=None) + .load() + .start() + .run_python_file('foo.py', ['foo.py', 'abc', '123']) + .stop() + .save() + """) + # -L sets a flag, and flags for the program don't confuse us. + self.cmd_executes("-x -p -L foo.py -a -b", """\ + .coverage(cover_pylib=True, data_suffix=True, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .load() + .start() + .run_python_file('foo.py', ['foo.py', '-a', '-b']) + .stop() + .save() + """) + + # Check that long forms of flags do the same thing as short forms. + self.cmd_executes_same("-x f.py", "--execute f.py") + self.cmd_executes_same("-e -x f.py", "--erase --execute f.py") + self.cmd_executes_same("-x -p f.py", "-x --parallel-mode f.py") + self.cmd_executes_same("-x -L f.py", "-x --pylib f.py") + + def test_combine(self): + # coverage -c + self.cmd_executes("-c", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .load() + .combine() + .save() + """) + self.cmd_executes_same("-c", "--combine") + + def test_report(self): + # coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] + self.cmd_executes("-r", self.INIT_LOAD + """\ + .report(ignore_errors=None, omit=None, include=None, morfs=[], + show_missing=None) + """) + self.cmd_executes("-r -i", self.INIT_LOAD + """\ + .report(ignore_errors=True, omit=None, include=None, morfs=[], + show_missing=None) + """) + self.cmd_executes("-r -m", self.INIT_LOAD + """\ + .report(ignore_errors=None, omit=None, include=None, morfs=[], + show_missing=True) + """) + self.cmd_executes("-r -o fooey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"]) + .load() + .report(ignore_errors=None, omit=["fooey"], include=None, + morfs=[], show_missing=None) + """) + self.cmd_executes("-r -o fooey,booey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"]) + .load() + .report(ignore_errors=None, omit=["fooey", "booey"], include=None, + morfs=[], show_missing=None) + """) + self.cmd_executes("-r mod1", self.INIT_LOAD + """\ + .report(ignore_errors=None, omit=None, include=None, + morfs=["mod1"], show_missing=None) + """) + self.cmd_executes("-r mod1 mod2 mod3", self.INIT_LOAD + """\ + .report(ignore_errors=None, omit=None, include=None, + morfs=["mod1", "mod2", "mod3"], show_missing=None) + """) + + self.cmd_executes_same("-r", "--report") + self.cmd_executes_same("-r -i", "-r --ignore-errors") + self.cmd_executes_same("-r -m", "-r --show-missing") + self.cmd_executes_same("-r -o f", "-r --omit=f") + self.cmd_executes_same("-r -o f", "-r --omit f") + self.cmd_executes_same("-r -o f,b", "-r --omit=f,b") + self.cmd_executes_same("-r -o f,b", "-r --omit f,b") + self.cmd_executes_same("-r -of", "-r --omit=f") + self.cmd_executes_same("-r -of,b", "-r --omit=f,b") + + def test_annotate(self): + # coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] + self.cmd_executes("-a", self.INIT_LOAD + """\ + .annotate(directory=None, ignore_errors=None, + omit=None, include=None, morfs=[]) + """) + self.cmd_executes("-a -d dir1", self.INIT_LOAD + """\ + .annotate(directory="dir1", ignore_errors=None, + omit=None, include=None, morfs=[]) + """) + self.cmd_executes("-a -i", self.INIT_LOAD + """\ + .annotate(directory=None, ignore_errors=True, + omit=None, include=None, morfs=[]) + """) + self.cmd_executes("-a -o fooey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"]) + .load() + .annotate(directory=None, ignore_errors=None, + omit=["fooey"], include=None, morfs=[]) + """) + self.cmd_executes("-a -o fooey,booey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"]) + .load() + .annotate(directory=None, ignore_errors=None, + omit=["fooey", "booey"], include=None, morfs=[]) + """) + self.cmd_executes("-a mod1", self.INIT_LOAD + """\ + .annotate(directory=None, ignore_errors=None, + omit=None, include=None, morfs=["mod1"]) + """) + self.cmd_executes("-a mod1 mod2 mod3", self.INIT_LOAD + """\ + .annotate(directory=None, ignore_errors=None, + omit=None, include=None, morfs=["mod1", "mod2", "mod3"]) + """) + + self.cmd_executes_same("-a", "--annotate") + self.cmd_executes_same("-a -d d1", "-a --directory=d1") + self.cmd_executes_same("-a -i", "-a --ignore-errors") + self.cmd_executes_same("-a -o f", "-a --omit=f") + self.cmd_executes_same("-a -o f", "-a --omit f") + self.cmd_executes_same("-a -o f,b", "-a --omit=f,b") + self.cmd_executes_same("-a -o f,b", "-a --omit f,b") + self.cmd_executes_same("-a -of", "-a --omit=f") + self.cmd_executes_same("-a -of,b", "-a --omit=f,b") + + def test_html_report(self): + # coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] + self.cmd_executes("-b", self.INIT_LOAD + """\ + .html_report(directory=None, ignore_errors=None, title=None, + omit=None, include=None, morfs=[]) + """) + self.cmd_executes("-b -d dir1", self.INIT_LOAD + """\ + .html_report(directory="dir1", ignore_errors=None, title=None, + omit=None, include=None, morfs=[]) + """) + self.cmd_executes("-b -i", self.INIT_LOAD + """\ + .html_report(directory=None, ignore_errors=True, title=None, + omit=None, include=None, morfs=[]) + """) + self.cmd_executes("-b -o fooey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"]) + .load() + .html_report(directory=None, ignore_errors=None, title=None, + omit=["fooey"], include=None, morfs=[]) + """) + self.cmd_executes("-b -o fooey,booey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"]) + .load() + .html_report(directory=None, ignore_errors=None, title=None, + omit=["fooey", "booey"], include=None, morfs=[]) + """) + self.cmd_executes("-b mod1", self.INIT_LOAD + """\ + .html_report(directory=None, ignore_errors=None, title=None, + omit=None, include=None, morfs=["mod1"]) + """) + self.cmd_executes("-b mod1 mod2 mod3", self.INIT_LOAD + """\ + .html_report(directory=None, ignore_errors=None, title=None, + omit=None, include=None, morfs=["mod1", "mod2", "mod3"]) + """) + + self.cmd_executes_same("-b", "--html") + self.cmd_executes_same("-b -d d1", "-b --directory=d1") + self.cmd_executes_same("-b -i", "-b --ignore-errors") + self.cmd_executes_same("-b -o f", "-b --omit=f") + self.cmd_executes_same("-b -o f,b", "-b --omit=f,b") + self.cmd_executes_same("-b -of", "-b --omit=f") + self.cmd_executes_same("-b -of,b", "-b --omit=f,b") + + def test_help(self): + # coverage -h + self.cmd_help("-h", topic="help", ret=OK) + self.cmd_help("--help", topic="help", ret=OK) + + def test_version(self): + # coverage --version + self.cmd_help("--version", topic="version", ret=OK) + + ## Error cases + + def test_argless_actions(self): + self.cmd_help("-e foo bar", "Unexpected arguments: foo bar") + self.cmd_help("-c baz quux", "Unexpected arguments: baz quux") + + def test_need_action(self): + self.cmd_help("-p", "You must specify at least one of " + "-e, -x, -c, -r, -a, or -b.") + + def test_bad_action_combinations(self): + self.cmd_help('-e -a', + "You can't specify the 'erase' and 'annotate' " + "options at the same time." + ) + self.cmd_help('-e -r', + "You can't specify the 'erase' and 'report' " + "options at the same time." + ) + self.cmd_help('-e -b', + "You can't specify the 'erase' and 'html' " + "options at the same time." + ) + self.cmd_help('-e -c', + "You can't specify the 'erase' and 'combine' " + "options at the same time." + ) + self.cmd_help('-x -a', + "You can't specify the 'execute' and 'annotate' " + "options at the same time." + ) + self.cmd_help('-x -r', + "You can't specify the 'execute' and 'report' " + "options at the same time." + ) + self.cmd_help('-x -b', + "You can't specify the 'execute' and 'html' " + "options at the same time." + ) + self.cmd_help('-x -c', + "You can't specify the 'execute' and 'combine' " + "options at the same time." + ) + + def test_nothing_to_do(self): + self.cmd_help("-x", "Nothing to do.") + + def test_unknown_option(self): + self.cmd_help("-z", "no such option: -z") + + +class FakeCoverageForDebugData(object): + """Just enough of a fake coverage package for the 'debug data' tests.""" + def __init__(self, summary): + self._summary = summary + self.filename = "FILENAME" + self.data = self + + # package members + def coverage(self, *unused_args, **unused_kwargs): + """The coverage class in the package.""" + return self + + # coverage methods + def load(self): + """Fake coverage().load()""" + pass + + # data methods + def has_arcs(self): + """Fake coverage().data.has_arcs()""" + return False + + def summary(self, fullpath): # pylint: disable=W0613 + """Fake coverage().data.summary()""" + return self._summary + + +class NewCmdLineTest(CmdLineTest): + """Tests of the coverage.py command line.""" + + def test_annotate(self): + self.cmd_executes_same("annotate", "-a") + self.cmd_executes_same("annotate -i", "-a -i") + self.cmd_executes_same("annotate -d d1", "-a -d d1") + self.cmd_executes_same("annotate --omit f", "-a --omit f") + self.cmd_executes_same("annotate --omit f,b", "-a --omit f,b") + self.cmd_executes_same("annotate m1", "-a m1") + self.cmd_executes_same("annotate m1 m2 m3", "-a m1 m2 m3") + + def test_combine(self): + self.cmd_executes_same("combine", "-c") + + def test_debug(self): + self.cmd_help("debug", "What information would you like: data, sys?") + self.cmd_help("debug foo", "Don't know what you mean by 'foo'") + + def test_debug_data(self): + fake = FakeCoverageForDebugData({ + 'file1.py': 17, 'file2.py': 23, + }) + self.command_line("debug data", _covpkg=fake) + self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ + -- data --------------------------------------- + path: FILENAME + has_arcs: False + + 2 files: + file1.py: 17 lines + file2.py: 23 lines + """)) + + def test_debug_data_with_no_data(self): + fake = FakeCoverageForDebugData({}) + self.command_line("debug data", _covpkg=fake) + self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ + -- data --------------------------------------- + path: FILENAME + has_arcs: False + No data collected + """)) + + def test_debug_sys(self): + self.command_line("debug sys") + out = self.stdout() + assert "version:" in out + assert "data_path:" in out + + def test_erase(self): + self.cmd_executes_same("erase", "-e") + + def test_help(self): + self.cmd_executes("help", ".help_fn(topic='help')") + + def test_cmd_help(self): + self.cmd_executes("run --help", + ".help_fn(parser='')") + self.cmd_executes_same("help run", "run --help") + + def test_html(self): + self.cmd_executes_same("html", "-b") + self.cmd_executes_same("html -i", "-b -i") + self.cmd_executes_same("html -d d1", "-b -d d1") + self.cmd_executes_same("html --omit f", "-b --omit f") + self.cmd_executes_same("html --omit f,b", "-b --omit f,b") + self.cmd_executes_same("html m1", "-b m1") + self.cmd_executes_same("html m1 m2 m3", "-b m1 m2 m3") + self.cmd_executes("html", self.INIT_LOAD + """\ + .html_report(ignore_errors=None, omit=None, include=None, morfs=[], + directory=None, title=None) + """) + self.cmd_executes("html --title=Hello_there", self.INIT_LOAD + """\ + .html_report(ignore_errors=None, omit=None, include=None, morfs=[], + directory=None, title='Hello_there') + """) + + def test_report(self): + self.cmd_executes_same("report", "-r") + self.cmd_executes_same("report -i", "-r -i") + self.cmd_executes_same("report -m", "-r -m") + self.cmd_executes_same("report --omit f", "-r --omit f") + self.cmd_executes_same("report --omit f,b", "-r --omit f,b") + self.cmd_executes_same("report m1", "-r m1") + self.cmd_executes_same("report m1 m2 m3", "-r m1 m2 m3") + + def test_run(self): + self.cmd_executes_same("run f.py", "-e -x f.py") + self.cmd_executes_same("run f.py -a arg -z", "-e -x f.py -a arg -z") + self.cmd_executes_same("run -a f.py", "-x f.py") + self.cmd_executes_same("run -p f.py", "-e -x -p f.py") + self.cmd_executes_same("run -L f.py", "-e -x -L f.py") + self.cmd_executes_same("run --timid f.py", "-e -x --timid f.py") + self.cmd_executes_same("run", "-x") + self.cmd_executes("run --branch foo.py", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=True, config_file=True, source=None, include=None, omit=None) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + self.cmd_executes("run --rcfile=myrc.rc foo.py", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file="myrc.rc", source=None, include=None, omit=None) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + self.cmd_executes("run --include=pre1,pre2 foo.py", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=["pre1", "pre2"], omit=None) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + self.cmd_executes("run --omit=opre1,opre2 foo.py", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["opre1", "opre2"]) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + self.cmd_executes("run --include=pre1,pre2 --omit=opre1,opre2 foo.py", + """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, + branch=None, config_file=True, source=None, + include=["pre1", "pre2"], + omit=["opre1", "opre2"]) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + self.cmd_executes("run --source=quux,hi.there,/home/bar foo.py", + """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, + branch=None, config_file=True, + source=["quux", "hi.there", "/home/bar"], include=None, + omit=None) + .erase() + .start() + .run_python_file('foo.py', ['foo.py']) + .stop() + .save() + """) + + def test_run_module(self): + self.cmd_executes("run -m mymodule", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .erase() + .start() + .run_python_module('mymodule', ['mymodule']) + .stop() + .save() + """) + self.cmd_executes("run -m mymodule -qq arg1 arg2", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=None) + .erase() + .start() + .run_python_module('mymodule', ['mymodule', '-qq', 'arg1', 'arg2']) + .stop() + .save() + """) + self.cmd_executes("run --branch -m mymodule", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=True, config_file=True, source=None, include=None, omit=None) + .erase() + .start() + .run_python_module('mymodule', ['mymodule']) + .stop() + .save() + """) + self.cmd_executes_same("run -m mymodule", "run --module mymodule") + + def test_xml(self): + # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] + self.cmd_executes("xml", self.INIT_LOAD + """\ + .xml_report(ignore_errors=None, omit=None, include=None, morfs=[], + outfile=None) + """) + self.cmd_executes("xml -i", self.INIT_LOAD + """\ + .xml_report(ignore_errors=True, omit=None, include=None, morfs=[], + outfile=None) + """) + self.cmd_executes("xml -o myxml.foo", self.INIT_LOAD + """\ + .xml_report(ignore_errors=None, omit=None, include=None, morfs=[], + outfile="myxml.foo") + """) + self.cmd_executes("xml -o -", self.INIT_LOAD + """\ + .xml_report(ignore_errors=None, omit=None, include=None, morfs=[], + outfile="-") + """) + self.cmd_executes("xml --omit fooey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey"]) + .load() + .xml_report(ignore_errors=None, omit=["fooey"], include=None, morfs=[], + outfile=None) + """) + self.cmd_executes("xml --omit fooey,booey", """\ + .coverage(cover_pylib=None, data_suffix=None, timid=None, branch=None, config_file=True, source=None, include=None, omit=["fooey", "booey"]) + .load() + .xml_report(ignore_errors=None, omit=["fooey", "booey"], include=None, + morfs=[], outfile=None) + """) + self.cmd_executes("xml mod1", self.INIT_LOAD + """\ + .xml_report(ignore_errors=None, omit=None, include=None, morfs=["mod1"], + outfile=None) + """) + self.cmd_executes("xml mod1 mod2 mod3", self.INIT_LOAD + """\ + .xml_report(ignore_errors=None, omit=None, include=None, + morfs=["mod1", "mod2", "mod3"], outfile=None) + """) + + def test_no_arguments_at_all(self): + self.cmd_help("", topic="minimum_help", ret=OK) + + def test_bad_command(self): + self.cmd_help("xyzzy", "Unknown command: 'xyzzy'") + + +class CmdLineStdoutTest(CmdLineTest): + """Test the command line with real stdout output.""" + + def test_minimum_help(self): + self.command_line("") + out = self.stdout() + assert "Code coverage for Python." in out + assert out.count("\n") < 4 + + def test_version(self): + self.command_line("--version") + out = self.stdout() + assert "ersion " in out + assert out.count("\n") < 4 + + def test_help(self): + self.command_line("help") + out = self.stdout() + assert "nedbatchelder.com" in out + assert out.count("\n") > 10 + + def test_cmd_help(self): + self.command_line("help run") + out = self.stdout() + assert "" in out + assert "--timid" in out + assert out.count("\n") > 10 + + def test_error(self): + self.command_line("fooey kablooey", ret=ERR) + out = self.stdout() + assert "fooey" in out + assert "help" in out + + +class CmdMainTest(CoverageTest): + """Tests of coverage.cmdline.main(), using mocking for isolation.""" + + class CoverageScriptStub(object): + """A stub for coverage.cmdline.CoverageScript, used by CmdMainTest.""" + + def command_line(self, argv): + """Stub for command_line, the arg determines what it will do.""" + if argv[0] == 'hello': + print("Hello, world!") + elif argv[0] == 'raise': + try: + raise Exception("oh noes!") + except: + raise ExceptionDuringRun(*sys.exc_info()) + elif argv[0] == 'internalraise': + raise ValueError("coverage is broken") + elif argv[0] == 'exit': + sys.exit(23) + else: + raise AssertionError("Bad CoverageScriptStub: %r"% (argv,)) + return 0 + + def setUp(self): + super(CmdMainTest, self).setUp() + self.old_CoverageScript = coverage.cmdline.CoverageScript + coverage.cmdline.CoverageScript = self.CoverageScriptStub + + def tearDown(self): + coverage.cmdline.CoverageScript = self.old_CoverageScript + super(CmdMainTest, self).tearDown() + + def test_normal(self): + ret = coverage.cmdline.main(['hello']) + self.assertEqual(ret, 0) + self.assertEqual(self.stdout(), "Hello, world!\n") + + def test_raise(self): + ret = coverage.cmdline.main(['raise']) + self.assertEqual(ret, 1) + self.assertEqual(self.stdout(), "") + err = self.stderr().split('\n') + self.assertEqual(err[0], 'Traceback (most recent call last):') + self.assertEqual(err[-3], ' raise Exception("oh noes!")') + self.assertEqual(err[-2], 'Exception: oh noes!') + + def test_internalraise(self): + self.assertRaisesRegexp(ValueError, + "coverage is broken", + coverage.cmdline.main, ['internalraise'] + ) + + def test_exit(self): + ret = coverage.cmdline.main(['exit']) + self.assertEqual(ret, 23) diff --git a/test/test_codeunit.py b/test/test_codeunit.py new file mode 100644 index 0000000..b4caff8 --- /dev/null +++ b/test/test_codeunit.py @@ -0,0 +1,103 @@ +"""Tests for coverage.codeunit""" + +import os, sys + +from coverage.codeunit import code_unit_factory +from coverage.files import FileLocator + +from test.coveragetest import CoverageTest + +# pylint: disable=F0401 +# Unable to import 'aa' (No module named aa) + +class CodeUnitTest(CoverageTest): + """Tests for coverage.codeunit""" + + run_in_temp_dir = False + + def setUp(self): + super(CodeUnitTest, self).setUp() + # Parent class saves and restores sys.path, we can just modify it. + testmods = self.nice_file(os.path.dirname(__file__), 'modules') + sys.path.append(testmods) + + def test_filenames(self): + acu = code_unit_factory("aa/afile.py", FileLocator()) + bcu = code_unit_factory("aa/bb/bfile.py", FileLocator()) + ccu = code_unit_factory("aa/bb/cc/cfile.py", FileLocator()) + self.assertEqual(acu[0].name, "aa/afile") + self.assertEqual(bcu[0].name, "aa/bb/bfile") + self.assertEqual(ccu[0].name, "aa/bb/cc/cfile") + self.assertEqual(acu[0].flat_rootname(), "aa_afile") + self.assertEqual(bcu[0].flat_rootname(), "aa_bb_bfile") + self.assertEqual(ccu[0].flat_rootname(), "aa_bb_cc_cfile") + self.assertEqual(acu[0].source_file().read(), "# afile.py\n") + self.assertEqual(bcu[0].source_file().read(), "# bfile.py\n") + self.assertEqual(ccu[0].source_file().read(), "# cfile.py\n") + + def test_odd_filenames(self): + acu = code_unit_factory("aa/afile.odd.py", FileLocator()) + bcu = code_unit_factory("aa/bb/bfile.odd.py", FileLocator()) + b2cu = code_unit_factory("aa/bb.odd/bfile.py", FileLocator()) + self.assertEqual(acu[0].name, "aa/afile.odd") + self.assertEqual(bcu[0].name, "aa/bb/bfile.odd") + self.assertEqual(b2cu[0].name, "aa/bb.odd/bfile") + self.assertEqual(acu[0].flat_rootname(), "aa_afile_odd") + self.assertEqual(bcu[0].flat_rootname(), "aa_bb_bfile_odd") + self.assertEqual(b2cu[0].flat_rootname(), "aa_bb_odd_bfile") + self.assertEqual(acu[0].source_file().read(), "# afile.odd.py\n") + self.assertEqual(bcu[0].source_file().read(), "# bfile.odd.py\n") + self.assertEqual(b2cu[0].source_file().read(), "# bfile.py\n") + + def test_modules(self): + import aa, aa.bb, aa.bb.cc + cu = code_unit_factory([aa, aa.bb, aa.bb.cc], FileLocator()) + self.assertEqual(cu[0].name, "aa") + self.assertEqual(cu[1].name, "aa.bb") + self.assertEqual(cu[2].name, "aa.bb.cc") + self.assertEqual(cu[0].flat_rootname(), "aa") + self.assertEqual(cu[1].flat_rootname(), "aa_bb") + self.assertEqual(cu[2].flat_rootname(), "aa_bb_cc") + self.assertEqual(cu[0].source_file().read(), "# aa\n") + self.assertEqual(cu[1].source_file().read(), "# bb\n") + self.assertEqual(cu[2].source_file().read(), "") # yes, empty + + def test_module_files(self): + import aa.afile, aa.bb.bfile, aa.bb.cc.cfile + cu = code_unit_factory([aa.afile, aa.bb.bfile, aa.bb.cc.cfile], + FileLocator()) + self.assertEqual(cu[0].name, "aa.afile") + self.assertEqual(cu[1].name, "aa.bb.bfile") + self.assertEqual(cu[2].name, "aa.bb.cc.cfile") + self.assertEqual(cu[0].flat_rootname(), "aa_afile") + self.assertEqual(cu[1].flat_rootname(), "aa_bb_bfile") + self.assertEqual(cu[2].flat_rootname(), "aa_bb_cc_cfile") + self.assertEqual(cu[0].source_file().read(), "# afile.py\n") + self.assertEqual(cu[1].source_file().read(), "# bfile.py\n") + self.assertEqual(cu[2].source_file().read(), "# cfile.py\n") + + def test_comparison(self): + acu = code_unit_factory("aa/afile.py", FileLocator())[0] + acu2 = code_unit_factory("aa/afile.py", FileLocator())[0] + zcu = code_unit_factory("aa/zfile.py", FileLocator())[0] + bcu = code_unit_factory("aa/bb/bfile.py", FileLocator())[0] + assert acu == acu2 and acu <= acu2 and acu >= acu2 + assert acu < zcu and acu <= zcu and acu != zcu + assert zcu > acu and zcu >= acu and zcu != acu + assert acu < bcu and acu <= bcu and acu != bcu + assert bcu > acu and bcu >= acu and bcu != acu + + def test_egg(self): + # Test that we can get files out of eggs, and read their source files. + # The egg1 module is installed by an action in igor.py. + import egg1, egg1.egg1 + # Verify that we really imported from an egg. If we did, then the + # __file__ won't be an actual file, because one of the "directories" + # in the path is actually the .egg zip file. + self.assert_doesnt_exist(egg1.__file__) + + cu = code_unit_factory([egg1, egg1.egg1], FileLocator()) + self.assertEqual(cu[0].source_file().read(), "") + self.assertEqual(cu[1].source_file().read().split("\n")[0], + "# My egg file!" + ) diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..1ca6376 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- +"""Test the config file handling for coverage.py""" + +import sys +import coverage +from coverage.misc import CoverageException + +from test.coveragetest import CoverageTest + + +class ConfigTest(CoverageTest): + """Tests of the different sources of configuration settings.""" + + def test_default_config(self): + # Just constructing a coverage() object gets the right defaults. + cov = coverage.coverage() + self.assertFalse(cov.config.timid) + self.assertFalse(cov.config.branch) + self.assertEqual(cov.config.data_file, ".coverage") + + def test_arguments(self): + # Arguments to the constructor are applied to the configuation. + cov = coverage.coverage(timid=True, data_file="fooey.dat") + self.assertTrue(cov.config.timid) + self.assertFalse(cov.config.branch) + self.assertEqual(cov.config.data_file, "fooey.dat") + + def test_config_file(self): + # A .coveragerc file will be read into the configuration. + self.make_file(".coveragerc", """\ + # This is just a bogus .rc file for testing. + [run] + timid = True + data_file = .hello_kitty.data + """) + cov = coverage.coverage() + self.assertTrue(cov.config.timid) + self.assertFalse(cov.config.branch) + self.assertEqual(cov.config.data_file, ".hello_kitty.data") + + def test_named_config_file(self): + # You can name the config file what you like. + self.make_file("my_cov.ini", """\ + [run] + timid = True + ; I wouldn't really use this as a data file... + data_file = delete.me + """) + cov = coverage.coverage(config_file="my_cov.ini") + self.assertTrue(cov.config.timid) + self.assertFalse(cov.config.branch) + self.assertEqual(cov.config.data_file, "delete.me") + + def test_ignored_config_file(self): + # You can disable reading the .coveragerc file. + self.make_file(".coveragerc", """\ + [run] + timid = True + data_file = delete.me + """) + cov = coverage.coverage(config_file=False) + self.assertFalse(cov.config.timid) + self.assertFalse(cov.config.branch) + self.assertEqual(cov.config.data_file, ".coverage") + + def test_config_file_then_args(self): + # The arguments override the .coveragerc file. + self.make_file(".coveragerc", """\ + [run] + timid = True + data_file = weirdo.file + """) + cov = coverage.coverage(timid=False, data_file=".mycov") + self.assertFalse(cov.config.timid) + self.assertFalse(cov.config.branch) + self.assertEqual(cov.config.data_file, ".mycov") + + def test_data_file_from_environment(self): + # There's an environment variable for the data_file. + self.make_file(".coveragerc", """\ + [run] + timid = True + data_file = weirdo.file + """) + self.set_environ("COVERAGE_FILE", "fromenv.dat") + cov = coverage.coverage() + self.assertEqual(cov.config.data_file, "fromenv.dat") + # But the constructor args override the env var. + cov = coverage.coverage(data_file="fromarg.dat") + self.assertEqual(cov.config.data_file, "fromarg.dat") + + def test_parse_errors(self): + # Im-parseable values raise CoverageException + self.make_file(".coveragerc", """\ + [run] + timid = maybe? + """) + self.assertRaises(CoverageException, coverage.coverage) + + def test_environment_vars_in_config(self): + # Config files can have $envvars in them. + self.make_file(".coveragerc", """\ + [run] + data_file = $DATA_FILE.fooey + branch = $OKAY + [report] + exclude_lines = + the_$$one + another${THING} + x${THING}y + x${NOTHING}y + huh$${X}what + """) + self.set_environ("DATA_FILE", "hello-world") + self.set_environ("THING", "ZZZ") + self.set_environ("OKAY", "yes") + cov = coverage.coverage() + self.assertEqual(cov.config.data_file, "hello-world.fooey") + self.assertEqual(cov.config.branch, True) + self.assertEqual(cov.config.exclude_list, + ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] + ) + + +class ConfigFileTest(CoverageTest): + """Tests of the config file settings in particular.""" + + def test_config_file_settings(self): + # This sample file tries to use lots of variation of syntax... + self.make_file(".coveragerc", """\ + # This is a settings file for coverage.py + [run] + timid = yes + data_file = something_or_other.dat + branch = 1 + cover_pylib = TRUE + parallel = on + include = a/ , b/ + + [report] + ; these settings affect reporting. + exclude_lines = + if 0: + + pragma:?\\s+no cover + another_tab + + ignore_errors = TRUE + omit = + one, another, some_more, + yet_more + precision = 3 + + partial_branches = + pragma:?\\s+no branch + partial_branches_always = + if 0: + while True: + + show_missing= TruE + + [html] + + directory = c:\\tricky\\dir.somewhere + extra_css=something/extra.css + title = Title & nums # nums! + [xml] + output=mycov.xml + + [paths] + source = + . + /home/ned/src/ + + other = other, /home/ned/other, c:\\Ned\\etc + + """) + cov = coverage.coverage() + + self.assertTrue(cov.config.timid) + self.assertEqual(cov.config.data_file, "something_or_other.dat") + self.assertTrue(cov.config.branch) + self.assertTrue(cov.config.cover_pylib) + self.assertTrue(cov.config.parallel) + + self.assertEqual(cov.get_exclude_list(), + ["if 0:", r"pragma:?\s+no cover", "another_tab"] + ) + self.assertTrue(cov.config.ignore_errors) + self.assertEqual(cov.config.include, ["a/", "b/"]) + self.assertEqual(cov.config.omit, + ["one", "another", "some_more", "yet_more"] + ) + self.assertEqual(cov.config.precision, 3) + + self.assertEqual(cov.config.partial_list, + [r"pragma:?\s+no branch"] + ) + self.assertEqual(cov.config.partial_always_list, + ["if 0:", "while True:"] + ) + self.assertTrue(cov.config.show_missing) + self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere") + self.assertEqual(cov.config.extra_css, "something/extra.css") + self.assertEqual(cov.config.html_title, "Title & nums # nums!") + + self.assertEqual(cov.config.xml_output, "mycov.xml") + + self.assertEqual(cov.config.paths, { + 'source': ['.', '/home/ned/src/'], + 'other': ['other', '/home/ned/other', 'c:\\Ned\\etc'] + }) + + if sys.version_info[:2] != (3,1): + def test_one(self): + # This sample file tries to use lots of variation of syntax... + self.make_file(".coveragerc", """\ + [html] + title = tabblo & «ταБЬℓσ» # numbers + """) + cov = coverage.coverage() + + self.assertEqual(cov.config.html_title, + "tabblo & «ταБЬℓσ» # numbers" + ) diff --git a/test/test_coverage.py b/test/test_coverage.py new file mode 100644 index 0000000..0918dfe --- /dev/null +++ b/test/test_coverage.py @@ -0,0 +1,1730 @@ +"""Tests for Coverage.""" +# http://nedbatchelder.com/code/coverage + +import sys +import coverage +from coverage.misc import CoverageException +from test.coveragetest import CoverageTest + + +class TestCoverageTest(CoverageTest): + """Make sure our complex self.check_coverage method works.""" + + def test_successful_coverage(self): + # The simplest run possible. + self.check_coverage("""\ + a = 1 + b = 2 + """, + [1,2] + ) + # You can provide a list of possible statement matches. + self.check_coverage("""\ + a = 1 + b = 2 + """, + ([100], [1,2], [1723,47]), + ) + # You can specify missing lines. + self.check_coverage("""\ + a = 1 + if a == 2: + a = 3 + """, + [1,2,3], + missing="3", + ) + # You can specify a list of possible missing lines. + self.check_coverage("""\ + a = 1 + if a == 2: + a = 3 + """, + [1,2,3], + missing=("47-49", "3", "100,102") + ) + + def test_failed_coverage(self): + # If the lines are wrong, the message shows right and wrong. + self.assertRaisesRegexp(AssertionError, + r"\[1, 2] != \[1]", + self.check_coverage, """\ + a = 1 + b = 2 + """, + [1] + ) + # If the list of lines possibilities is wrong, the msg shows right. + self.assertRaisesRegexp(AssertionError, + r"None of the lines choices matched \[1, 2]", + self.check_coverage, """\ + a = 1 + b = 2 + """, + ([1], [2]) + ) + # If the missing lines are wrong, the message shows right and wrong. + self.assertRaisesRegexp(AssertionError, + r"'3' != '37'", + self.check_coverage, """\ + a = 1 + if a == 2: + a = 3 + """, + [1,2,3], + missing="37", + ) + # If the missing lines possibilities are wrong, the msg shows right. + self.assertRaisesRegexp(AssertionError, + r"None of the missing choices matched '3'", + self.check_coverage, """\ + a = 1 + if a == 2: + a = 3 + """, + [1,2,3], + missing=("37", "4-10"), + ) + + +class BasicCoverageTest(CoverageTest): + """The simplest tests, for quick smoke testing of fundamental changes.""" + + def test_simple(self): + self.check_coverage("""\ + a = 1 + b = 2 + + c = 4 + # Nothing here + d = 6 + """, + [1,2,4,6], report="4 0 100%") + + def test_indentation_wackiness(self): + # Partial final lines are OK. + self.check_coverage("""\ + import sys + if not sys.path: + a = 1 + """, + [1,2,3], "3") + + def test_multiline_initializer(self): + self.check_coverage("""\ + d = { + 'foo': 1+2, + 'bar': (lambda x: x+1)(1), + 'baz': str(1), + } + + e = { 'foo': 1, 'bar': 2 } + """, + [1,7], "") + + def test_list_comprehension(self): + self.check_coverage("""\ + l = [ + 2*i for i in range(10) + if i > 5 + ] + assert l == [12, 14, 16, 18] + """, + [1,5], "") + + +class SimpleStatementTest(CoverageTest): + """Testing simple single-line statements.""" + + def test_expression(self): + # Bare expressions as statements are tricky: some implementations + # optimize some of them away. All implementations seem to count + # the implicit return at the end as executable. + self.check_coverage("""\ + 12 + 23 + """, + ([1,2],[2]), "") + self.check_coverage("""\ + 12 + 23 + a = 3 + """, + ([1,2,3],[3]), "") + self.check_coverage("""\ + 1 + 2 + 1 + \\ + 2 + """, + ([1,2], [2]), "") + self.check_coverage("""\ + 1 + 2 + 1 + \\ + 2 + a = 4 + """, + ([1,2,4], [4]), "") + + def test_assert(self): + self.check_coverage("""\ + assert (1 + 2) + assert (1 + + 2) + assert (1 + 2), 'the universe is broken' + assert (1 + + 2), \\ + 'something is amiss' + """, + [1,2,4,5], "") + + def test_assignment(self): + # Simple variable assignment + self.check_coverage("""\ + a = (1 + 2) + b = (1 + + 2) + c = \\ + 1 + """, + [1,2,4], "") + + def test_assign_tuple(self): + self.check_coverage("""\ + a = 1 + a,b,c = 7,8,9 + assert a == 7 and b == 8 and c == 9 + """, + [1,2,3], "") + + def test_attribute_assignment(self): + # Attribute assignment + self.check_coverage("""\ + class obj: pass + o = obj() + o.foo = (1 + 2) + o.foo = (1 + + 2) + o.foo = \\ + 1 + """, + [1,2,3,4,6], "") + + def test_list_of_attribute_assignment(self): + self.check_coverage("""\ + class obj: pass + o = obj() + o.a, o.b = (1 + 2), 3 + o.a, o.b = (1 + + 2), (3 + + 4) + o.a, o.b = \\ + 1, \\ + 2 + """, + [1,2,3,4,7], "") + + def test_augmented_assignment(self): + self.check_coverage("""\ + a = 1 + a += 1 + a += (1 + + 2) + a += \\ + 1 + """, + [1,2,3,5], "") + + def test_triple_string_stuff(self): + self.check_coverage("""\ + a = ''' + a multiline + string. + ''' + b = ''' + long expression + ''' + ''' + on many + lines. + ''' + c = len(''' + long expression + ''' + + ''' + on many + lines. + ''') + """, + [1,5,11], "") + + def test_pass(self): + # pass is tricky: if it's the only statement in a block, then it is + # "executed". But if it is not the only statement, then it is not. + self.check_coverage("""\ + if 1==1: + pass + """, + [1,2], "") + self.check_coverage("""\ + def foo(): + pass + foo() + """, + [1,2,3], "") + self.check_coverage("""\ + def foo(): + "doc" + pass + foo() + """, + ([1,3,4], [1,4]), "") + self.check_coverage("""\ + class Foo: + def foo(self): + pass + Foo().foo() + """, + [1,2,3,4], "") + self.check_coverage("""\ + class Foo: + def foo(self): + "Huh?" + pass + Foo().foo() + """, + ([1,2,4,5], [1,2,5]), "") + + def test_del(self): + self.check_coverage("""\ + d = { 'a': 1, 'b': 1, 'c': 1, 'd': 1, 'e': 1 } + del d['a'] + del d[ + 'b' + ] + del d['c'], \\ + d['d'], \\ + d['e'] + assert(len(d.keys()) == 0) + """, + [1,2,3,6,9], "") + + if sys.version_info < (3, 0): # Print statement is gone in Py3k. + def test_print(self): + self.check_coverage("""\ + print "hello, world!" + print ("hey: %d" % + 17) + print "goodbye" + print "hello, world!", + print ("hey: %d" % + 17), + print "goodbye", + """, + [1,2,4,5,6,8], "") + + def test_raise(self): + self.check_coverage("""\ + try: + raise Exception( + "hello %d" % + 17) + except: + pass + """, + [1,2,5,6], "") + + def test_return(self): + self.check_coverage("""\ + def fn(): + a = 1 + return a + + x = fn() + assert(x == 1) + """, + [1,2,3,5,6], "") + self.check_coverage("""\ + def fn(): + a = 1 + return ( + a + + 1) + + x = fn() + assert(x == 2) + """, + [1,2,3,7,8], "") + self.check_coverage("""\ + def fn(): + a = 1 + return (a, + a + 1, + a + 2) + + x,y,z = fn() + assert x == 1 and y == 2 and z == 3 + """, + [1,2,3,7,8], "") + + def test_yield(self): + self.check_coverage("""\ + from __future__ import generators + def gen(): + yield 1 + yield (2+ + 3+ + 4) + yield 1, \\ + 2 + a,b,c = gen() + assert a == 1 and b == 9 and c == (1,2) + """, + [1,2,3,4,7,9,10], "") + + def test_break(self): + self.check_coverage("""\ + for x in range(10): + a = 2 + x + break + a = 4 + assert a == 2 + """, + [1,2,3,4,5], "4") + + def test_continue(self): + self.check_coverage("""\ + for x in range(10): + a = 2 + x + continue + a = 4 + assert a == 11 + """, + [1,2,3,4,5], "4") + + if 0: # expected failure + # Peephole optimization of jumps to jumps can mean that some statements + # never hit the line tracer. The behavior is different in different + # versions of Python, so don't run this test: + def test_strange_unexecuted_continue(self): + self.check_coverage("""\ + a = b = c = 0 + for n in range(100): + if n % 2: + if n % 4: + a += 1 + continue # <-- This line may not be hit. + else: + b += 1 + c += 1 + assert a == 50 and b == 50 and c == 50 + + a = b = c = 0 + for n in range(100): + if n % 2: + if n % 3: + a += 1 + continue # <-- This line is always hit. + else: + b += 1 + c += 1 + assert a == 33 and b == 50 and c == 50 + """, + [1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], "") + + def test_import(self): + self.check_coverage("""\ + import string + from sys import path + a = 1 + """, + [1,2,3], "") + self.check_coverage("""\ + import string + if 1 == 2: + from sys import path + a = 1 + """, + [1,2,3,4], "3") + self.check_coverage("""\ + import string, \\ + os, \\ + re + from sys import path, \\ + stdout + a = 1 + """, + [1,4,6], "") + self.check_coverage("""\ + import sys, sys as s + assert s.path == sys.path + """, + [1,2], "") + self.check_coverage("""\ + import sys, \\ + sys as s + assert s.path == sys.path + """, + [1,3], "") + self.check_coverage("""\ + from sys import path, \\ + path as p + assert p == path + """, + [1,3], "") + self.check_coverage("""\ + from sys import \\ + * + assert len(path) > 0 + """, + [1,3], "") + + def test_global(self): + self.check_coverage("""\ + g = h = i = 1 + def fn(): + global g + global h, \\ + i + g = h = i = 2 + fn() + assert g == 2 and h == 2 and i == 2 + """, + [1,2,6,7,8], "") + self.check_coverage("""\ + g = h = i = 1 + def fn(): + global g; g = 2 + fn() + assert g == 2 and h == 1 and i == 1 + """, + [1,2,3,4,5], "") + + if sys.version_info < (3, 0): + # In Python 2.x, exec is a statement. + def test_exec(self): + self.check_coverage("""\ + a = b = c = 1 + exec "a = 2" + exec ("b = " + + "c = " + + "2") + assert a == 2 and b == 2 and c == 2 + """, + [1,2,3,6], "") + self.check_coverage("""\ + vars = {'a': 1, 'b': 1, 'c': 1} + exec "a = 2" in vars + exec ("b = " + + "c = " + + "2") in vars + assert vars['a'] == 2 and vars['b'] == 2 and vars['c'] == 2 + """, + [1,2,3,6], "") + self.check_coverage("""\ + globs = {} + locs = {'a': 1, 'b': 1, 'c': 1} + exec "a = 2" in globs, locs + exec ("b = " + + "c = " + + "2") in globs, locs + assert locs['a'] == 2 and locs['b'] == 2 and locs['c'] == 2 + """, + [1,2,3,4,7], "") + else: + # In Python 3.x, exec is a function. + def test_exec(self): + self.check_coverage("""\ + a = b = c = 1 + exec("a = 2") + exec("b = " + + "c = " + + "2") + assert a == 2 and b == 2 and c == 2 + """, + [1,2,3,6], "") + self.check_coverage("""\ + vars = {'a': 1, 'b': 1, 'c': 1} + exec("a = 2", vars) + exec("b = " + + "c = " + + "2", vars) + assert vars['a'] == 2 and vars['b'] == 2 and vars['c'] == 2 + """, + [1,2,3,6], "") + self.check_coverage("""\ + globs = {} + locs = {'a': 1, 'b': 1, 'c': 1} + exec("a = 2", globs, locs) + exec("b = " + + "c = " + + "2", globs, locs) + assert locs['a'] == 2 and locs['b'] == 2 and locs['c'] == 2 + """, + [1,2,3,4,7], "") + + def test_extra_doc_string(self): + self.check_coverage("""\ + a = 1 + "An extra docstring, should be a comment." + b = 3 + assert (a,b) == (1,3) + """, + [1,3,4], "") + self.check_coverage("""\ + a = 1 + "An extra docstring, should be a comment." + b = 3 + 123 # A number for some reason: ignored + 1+1 # An expression: executed. + c = 6 + assert (a,b,c) == (1,3,6) + """, + ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7]), "") + + +class CompoundStatementTest(CoverageTest): + """Testing coverage of multi-line compound statements.""" + + def test_statement_list(self): + self.check_coverage("""\ + a = 1; + b = 2; c = 3 + d = 4; e = 5; + + assert (a,b,c,d,e) == (1,2,3,4,5) + """, + [1,2,3,5], "") + + def test_if(self): + self.check_coverage("""\ + a = 1 + if a == 1: + x = 3 + assert x == 3 + if (a == + 1): + x = 7 + assert x == 7 + """, + [1,2,3,4,5,7,8], "") + self.check_coverage("""\ + a = 1 + if a == 1: + x = 3 + else: + y = 5 + assert x == 3 + """, + [1,2,3,5,6], "5") + self.check_coverage("""\ + a = 1 + if a != 1: + x = 3 + else: + y = 5 + assert y == 5 + """, + [1,2,3,5,6], "3") + self.check_coverage("""\ + a = 1; b = 2 + if a == 1: + if b == 2: + x = 4 + else: + y = 6 + else: + z = 8 + assert x == 4 + """, + [1,2,3,4,6,8,9], "6-8") + + def test_elif(self): + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a == 1: + x = 3 + elif b == 2: + y = 5 + else: + z = 7 + assert x == 3 + """, + [1,2,3,4,5,7,8], "4-7", report="7 3 57% 4-7") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a != 1: + x = 3 + elif b == 2: + y = 5 + else: + z = 7 + assert y == 5 + """, + [1,2,3,4,5,7,8], "3, 7", report="7 2 71% 3, 7") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a != 1: + x = 3 + elif b != 2: + y = 5 + else: + z = 7 + assert z == 7 + """, + [1,2,3,4,5,7,8], "3, 5", report="7 2 71% 3, 5") + + def test_elif_no_else(self): + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a == 1: + x = 3 + elif b == 2: + y = 5 + assert x == 3 + """, + [1,2,3,4,5,6], "4-5", report="6 2 67% 4-5") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a != 1: + x = 3 + elif b == 2: + y = 5 + assert y == 5 + """, + [1,2,3,4,5,6], "3", report="6 1 83% 3") + + def test_elif_bizarre(self): + self.check_coverage("""\ + def f(self): + if self==1: + x = 3 + elif self.m('fred'): + x = 5 + elif (g==1) and (b==2): + x = 7 + elif self.m('fred')==True: + x = 9 + elif ((g==1) and (b==2))==True: + x = 11 + else: + x = 13 + """, + [1,2,3,4,5,6,7,8,9,10,11,13], "2-13") + + def test_split_if(self): + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if \\ + a == 1: + x = 3 + elif \\ + b == 2: + y = 5 + else: + z = 7 + assert x == 3 + """, + [1,2,4,5,7,9,10], "5-9") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if \\ + a != 1: + x = 3 + elif \\ + b == 2: + y = 5 + else: + z = 7 + assert y == 5 + """, + [1,2,4,5,7,9,10], "4, 9") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if \\ + a != 1: + x = 3 + elif \\ + b != 2: + y = 5 + else: + z = 7 + assert z == 7 + """, + [1,2,4,5,7,9,10], "4, 7") + + def test_pathological_split_if(self): + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if ( + a == 1 + ): + x = 3 + elif ( + b == 2 + ): + y = 5 + else: + z = 7 + assert x == 3 + """, + [1,2,5,6,9,11,12], "6-11") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if ( + a != 1 + ): + x = 3 + elif ( + b == 2 + ): + y = 5 + else: + z = 7 + assert y == 5 + """, + [1,2,5,6,9,11,12], "5, 11") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if ( + a != 1 + ): + x = 3 + elif ( + b != 2 + ): + y = 5 + else: + z = 7 + assert z == 7 + """, + [1,2,5,6,9,11,12], "5, 9") + + def test_absurd_split_if(self): + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a == 1 \\ + : + x = 3 + elif b == 2 \\ + : + y = 5 + else: + z = 7 + assert x == 3 + """, + [1,2,4,5,7,9,10], "5-9") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a != 1 \\ + : + x = 3 + elif b == 2 \\ + : + y = 5 + else: + z = 7 + assert y == 5 + """, + [1,2,4,5,7,9,10], "4, 9") + self.check_coverage("""\ + a = 1; b = 2; c = 3; + if a != 1 \\ + : + x = 3 + elif b != 2 \\ + : + y = 5 + else: + z = 7 + assert z == 7 + """, + [1,2,4,5,7,9,10], "4, 7") + + if sys.version_info >= (2, 4): + # In 2.4 and up, constant if's were compiled away. + def test_constant_if(self): + self.check_coverage("""\ + if 1: + a = 2 + assert a == 2 + """, + [2,3], "") + + def test_while(self): + self.check_coverage("""\ + a = 3; b = 0 + while a: + b += 1 + a -= 1 + assert a == 0 and b == 3 + """, + [1,2,3,4,5], "") + self.check_coverage("""\ + a = 3; b = 0 + while a: + b += 1 + break + b = 99 + assert a == 3 and b == 1 + """, + [1,2,3,4,5,6], "5") + + def test_while_else(self): + # Take the else branch. + self.check_coverage("""\ + a = 3; b = 0 + while a: + b += 1 + a -= 1 + else: + b = 99 + assert a == 0 and b == 99 + """, + [1,2,3,4,6,7], "") + # Don't take the else branch. + self.check_coverage("""\ + a = 3; b = 0 + while a: + b += 1 + a -= 1 + break + b = 123 + else: + b = 99 + assert a == 2 and b == 1 + """, + [1,2,3,4,5,6,8,9], "6-8") + + def test_split_while(self): + self.check_coverage("""\ + a = 3; b = 0 + while \\ + a: + b += 1 + a -= 1 + assert a == 0 and b == 3 + """, + [1,2,4,5,6], "") + self.check_coverage("""\ + a = 3; b = 0 + while ( + a + ): + b += 1 + a -= 1 + assert a == 0 and b == 3 + """, + [1,2,5,6,7], "") + + def test_for(self): + self.check_coverage("""\ + a = 0 + for i in [1,2,3,4,5]: + a += i + assert a == 15 + """, + [1,2,3,4], "") + self.check_coverage("""\ + a = 0 + for i in [1, + 2,3,4, + 5]: + a += i + assert a == 15 + """, + [1,2,5,6], "") + self.check_coverage("""\ + a = 0 + for i in [1,2,3,4,5]: + a += i + break + a = 99 + assert a == 1 + """, + [1,2,3,4,5,6], "5") + + def test_for_else(self): + self.check_coverage("""\ + a = 0 + for i in range(5): + a += i+1 + else: + a = 99 + assert a == 99 + """, + [1,2,3,5,6], "") + self.check_coverage("""\ + a = 0 + for i in range(5): + a += i+1 + break + a = 99 + else: + a = 123 + assert a == 1 + """, + [1,2,3,4,5,7,8], "5-7") + + def test_split_for(self): + self.check_coverage("""\ + a = 0 + for \\ + i in [1,2,3,4,5]: + a += i + assert a == 15 + """, + [1,2,4,5], "") + self.check_coverage("""\ + a = 0 + for \\ + i in [1, + 2,3,4, + 5]: + a += i + assert a == 15 + """, + [1,2,6,7], "") + + def test_try_except(self): + self.check_coverage("""\ + a = 0 + try: + a = 1 + except: + a = 99 + assert a == 1 + """, + [1,2,3,4,5,6], "4-5") + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + assert a == 99 + """, + [1,2,3,4,5,6,7], "") + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except ImportError: + a = 99 + except: + a = 123 + assert a == 123 + """, + [1,2,3,4,5,6,7,8,9], "6") + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise IOError("foo") + except ImportError: + a = 99 + except IOError: + a = 17 + except: + a = 123 + assert a == 17 + """, + [1,2,3,4,5,6,7,8,9,10,11], "6, 9-10") + self.check_coverage("""\ + a = 0 + try: + a = 1 + except: + a = 99 + else: + a = 123 + assert a == 123 + """, + [1,2,3,4,5,7,8], "4-5") + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + else: + a = 123 + assert a == 99 + """, + [1,2,3,4,5,6,8,9], "8") + + def test_try_finally(self): + self.check_coverage("""\ + a = 0 + try: + a = 1 + finally: + a = 99 + assert a == 99 + """, + [1,2,3,5,6], "") + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + try: + raise Exception("foo") + finally: + b = 123 + except: + a = 99 + assert a == 99 and b == 123 + """, + [1,2,3,4,5,7,8,9,10], "") + + def test_function_def(self): + self.check_coverage("""\ + a = 99 + def foo(): + ''' docstring + ''' + return 1 + + a = foo() + assert a == 1 + """, + [1,2,5,7,8], "") + self.check_coverage("""\ + def foo( + a, + b + ): + ''' docstring + ''' + return a+b + + x = foo(17, 23) + assert x == 40 + """, + [1,7,9,10], "") + self.check_coverage("""\ + def foo( + a = (lambda x: x*2)(10), + b = ( + lambda x: + x+1 + )(1) + ): + ''' docstring + ''' + return a+b + + x = foo() + assert x == 22 + """, + [1,10,12,13], "") + + def test_class_def(self): + self.check_coverage("""\ + # A comment. + class theClass: + ''' the docstring. + Don't be fooled. + ''' + def __init__(self): + ''' Another docstring. ''' + self.a = 1 + + def foo(self): + return self.a + + x = theClass().foo() + assert x == 1 + """, + [2,6,8,10,11,13,14], "") + + +class ExcludeTest(CoverageTest): + """Tests of the exclusion feature to mark lines as not covered.""" + + def test_default(self): + # A number of forms of pragma comment are accepted. + self.check_coverage("""\ + a = 1 + b = 2 # pragma: no cover + c = 3 + d = 4 #pragma NOCOVER + e = 5 + """, + [1,3,5] + ) + + def test_simple(self): + self.check_coverage("""\ + a = 1; b = 2 + + if 0: + a = 4 # -cc + """, + [1,3], "", excludes=['-cc']) + + def test_two_excludes(self): + self.check_coverage("""\ + a = 1; b = 2 + + if a == 99: + a = 4 # -cc + b = 5 + c = 6 # -xx + assert a == 1 and b == 2 + """, + [1,3,5,7], "5", excludes=['-cc', '-xx']) + + def test_excluding_if_suite(self): + self.check_coverage("""\ + a = 1; b = 2 + + if 0: + a = 4 + b = 5 + c = 6 + assert a == 1 and b == 2 + """, + [1,7], "", excludes=['if 0:']) + + def test_excluding_if_but_not_else_suite(self): + self.check_coverage("""\ + a = 1; b = 2 + + if 0: + a = 4 + b = 5 + c = 6 + else: + a = 8 + b = 9 + assert a == 8 and b == 9 + """, + [1,8,9,10], "", excludes=['if 0:']) + + def test_excluding_else_suite(self): + self.check_coverage("""\ + a = 1; b = 2 + + if 1==1: + a = 4 + b = 5 + c = 6 + else: #pragma: NO COVER + a = 8 + b = 9 + assert a == 4 and b == 5 and c == 6 + """, + [1,3,4,5,6,10], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 1; b = 2 + + if 1==1: + a = 4 + b = 5 + c = 6 + + # Lots of comments to confuse the else handler. + # more. + + else: #pragma: NO COVER + + # Comments here too. + + a = 8 + b = 9 + assert a == 4 and b == 5 and c == 6 + """, + [1,3,4,5,6,17], "", excludes=['#pragma: NO COVER']) + + def test_excluding_elif_suites(self): + self.check_coverage("""\ + a = 1; b = 2 + + if 1==1: + a = 4 + b = 5 + c = 6 + elif 1==0: #pragma: NO COVER + a = 8 + b = 9 + else: + a = 11 + b = 12 + assert a == 4 and b == 5 and c == 6 + """, + [1,3,4,5,6,11,12,13], "11-12", excludes=['#pragma: NO COVER']) + + def test_excluding_oneline_if(self): + self.check_coverage("""\ + def foo(): + a = 2 + if 0: x = 3 # no cover + b = 4 + + foo() + """, + [1,2,4,6], "", excludes=["no cover"]) + + def test_excluding_a_colon_not_a_suite(self): + self.check_coverage("""\ + def foo(): + l = list(range(10)) + a = l[:3] # no cover + b = 4 + + foo() + """, + [1,2,4,6], "", excludes=["no cover"]) + + def test_excluding_for_suite(self): + self.check_coverage("""\ + a = 0 + for i in [1,2,3,4,5]: #pragma: NO COVER + a += i + assert a == 15 + """, + [1,4], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + for i in [1, + 2,3,4, + 5]: #pragma: NO COVER + a += i + assert a == 15 + """, + [1,6], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + for i in [1,2,3,4,5 + ]: #pragma: NO COVER + a += i + break + a = 99 + assert a == 1 + """, + [1,7], "", excludes=['#pragma: NO COVER']) + + def test_excluding_for_else(self): + self.check_coverage("""\ + a = 0 + for i in range(5): + a += i+1 + break + a = 99 + else: #pragma: NO COVER + a = 123 + assert a == 1 + """, + [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) + + def test_excluding_while(self): + self.check_coverage("""\ + a = 3; b = 0 + while a*b: #pragma: NO COVER + b += 1 + break + b = 99 + assert a == 3 and b == 0 + """, + [1,6], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 3; b = 0 + while ( + a*b + ): #pragma: NO COVER + b += 1 + break + b = 99 + assert a == 3 and b == 0 + """, + [1,8], "", excludes=['#pragma: NO COVER']) + + def test_excluding_while_else(self): + self.check_coverage("""\ + a = 3; b = 0 + while a: + b += 1 + break + b = 99 + else: #pragma: NO COVER + b = 123 + assert a == 3 and b == 1 + """, + [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) + + def test_excluding_try_except(self): + self.check_coverage("""\ + a = 0 + try: + a = 1 + except: #pragma: NO COVER + a = 99 + assert a == 1 + """, + [1,2,3,6], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + assert a == 99 + """, + [1,2,3,4,5,6,7], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except ImportError: #pragma: NO COVER + a = 99 + except: + a = 123 + assert a == 123 + """, + [1,2,3,4,7,8,9], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + except: #pragma: NO COVER + a = 99 + else: + a = 123 + assert a == 123 + """, + [1,2,3,7,8], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + else: #pragma: NO COVER + a = 123 + assert a == 99 + """, + [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER']) + + def test_excluding_try_except_pass(self): + self.check_coverage("""\ + a = 0 + try: + a = 1 + except: #pragma: NO COVER + x = 2 + assert a == 1 + """, + [1,2,3,6], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except ImportError: #pragma: NO COVER + x = 2 + except: + a = 123 + assert a == 123 + """, + [1,2,3,4,7,8,9], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + except: #pragma: NO COVER + x = 2 + else: + a = 123 + assert a == 123 + """, + [1,2,3,7,8], "", excludes=['#pragma: NO COVER']) + self.check_coverage("""\ + a = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + else: #pragma: NO COVER + x = 2 + assert a == 99 + """, + [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER']) + + def test_excluding_if_pass(self): + # From a comment on the coverage page by Michael McNeil Forbes: + self.check_coverage("""\ + def f(): + if False: # pragma: no cover + pass # This line still reported as missing + if False: # pragma: no cover + x = 1 # Now it is skipped. + + f() + """, + [1,7], "", excludes=["no cover"]) + + def test_excluding_function(self): + self.check_coverage("""\ + def fn(foo): #pragma: NO COVER + a = 1 + b = 2 + c = 3 + + x = 1 + assert x == 1 + """, + [6,7], "", excludes=['#pragma: NO COVER']) + + def test_excluding_method(self): + self.check_coverage("""\ + class Fooey: + def __init__(self): + self.a = 1 + + def foo(self): #pragma: NO COVER + return self.a + + x = Fooey() + assert x.a == 1 + """, + [1,2,3,8,9], "", excludes=['#pragma: NO COVER']) + + def test_excluding_class(self): + self.check_coverage("""\ + class Fooey: #pragma: NO COVER + def __init__(self): + self.a = 1 + + def foo(self): + return self.a + + x = 1 + assert x == 1 + """, + [8,9], "", excludes=['#pragma: NO COVER']) + + +if sys.version_info >= (2, 4): + class Py24Test(CoverageTest): + """Tests of new syntax in Python 2.4.""" + + def test_function_decorators(self): + self.check_coverage("""\ + def require_int(func): + def wrapper(arg): + assert isinstance(arg, int) + return func(arg) + + return wrapper + + @require_int + def p1(arg): + return arg*2 + + assert p1(10) == 20 + """, + [1,2,3,4,6,8,10,12], "") + + def test_function_decorators_with_args(self): + self.check_coverage("""\ + def boost_by(extra): + def decorator(func): + def wrapper(arg): + return extra*func(arg) + return wrapper + return decorator + + @boost_by(10) + def boosted(arg): + return arg*2 + + assert boosted(10) == 200 + """, + [1,2,3,4,5,6,8,10,12], "") + + def test_double_function_decorators(self): + self.check_coverage("""\ + def require_int(func): + def wrapper(arg): + assert isinstance(arg, int) + return func(arg) + return wrapper + + def boost_by(extra): + def decorator(func): + def wrapper(arg): + return extra*func(arg) + return wrapper + return decorator + + @require_int + @boost_by(10) + def boosted1(arg): + return arg*2 + + assert boosted1(10) == 200 + + @boost_by(10) + @require_int + def boosted2(arg): + return arg*2 + + assert boosted2(10) == 200 + """, + ([1,2,3,4,5,7,8,9,10,11,12,14,15,17,19,21,22,24,26], + [1,2,3,4,5,7,8,9,10,11,12,14, 17,19,21, 24,26]), "") + + +if sys.version_info >= (2, 5): + class Py25Test(CoverageTest): + """Tests of new syntax in Python 2.5.""" + + def test_with_statement(self): + self.check_coverage("""\ + from __future__ import with_statement + + class Managed: + def __enter__(self): + desc = "enter" + + def __exit__(self, type, value, tb): + desc = "exit" + + m = Managed() + with m: + desc = "block1a" + desc = "block1b" + + try: + with m: + desc = "block2" + raise Exception("Boo!") + except: + desc = "caught" + """, + [1,3,4,5,7,8,10,11,12,13,15,16,17,18,19,20], "") + + def test_try_except_finally(self): + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + except: + a = 99 + finally: + b = 2 + assert a == 1 and b == 2 + """, + [1,2,3,4,5,7,8], "4-5") + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + finally: + b = 2 + assert a == 99 and b == 2 + """, + [1,2,3,4,5,6,8,9], "") + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + raise Exception("foo") + except ImportError: + a = 99 + except: + a = 123 + finally: + b = 2 + assert a == 123 and b == 2 + """, + [1,2,3,4,5,6,7,8,10,11], "6") + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + raise IOError("foo") + except ImportError: + a = 99 + except IOError: + a = 17 + except: + a = 123 + finally: + b = 2 + assert a == 17 and b == 2 + """, + [1,2,3,4,5,6,7,8,9,10,12,13], "6, 9-10") + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + except: + a = 99 + else: + a = 123 + finally: + b = 2 + assert a == 123 and b == 2 + """, + [1,2,3,4,5,7,9,10], "4-5") + self.check_coverage("""\ + a = 0; b = 0 + try: + a = 1 + raise Exception("foo") + except: + a = 99 + else: + a = 123 + finally: + b = 2 + assert a == 99 and b == 2 + """, + [1,2,3,4,5,6,8,10,11], "8") + + +class ModuleTest(CoverageTest): + """Tests for the module-level behavior of the `coverage` module.""" + + def test_not_singleton(self): + # You *can* create another coverage object. + coverage.coverage() + coverage.coverage() + + +class ReportingTest(CoverageTest): + """Tests of some reporting behavior.""" + + def test_no_data_to_report_on_annotate(self): + # Reporting with no data produces a nice message and no output dir. + self.assertRaisesRegexp( + CoverageException, "No data to report.", + self.command_line, "annotate -d ann" + ) + self.assert_doesnt_exist("ann") + + def test_no_data_to_report_on_html(self): + # Reporting with no data produces a nice message and no output dir. + self.assertRaisesRegexp( + CoverageException, "No data to report.", + self.command_line, "html -d htmlcov" + ) + self.assert_doesnt_exist("htmlcov") + + def test_no_data_to_report_on_xml(self): + # Reporting with no data produces a nice message. + self.assertRaisesRegexp( + CoverageException, "No data to report.", + self.command_line, "xml" + ) + # Currently, this leaves an empty coverage.xml file... :( diff --git a/test/test_data.py b/test/test_data.py new file mode 100644 index 0000000..9281ccc --- /dev/null +++ b/test/test_data.py @@ -0,0 +1,146 @@ +"""Tests for coverage.data""" + +from coverage.backward import pickle +from coverage.data import CoverageData +from coverage.files import PathAliases + +from test.coveragetest import CoverageTest + + +DATA_1 = { 'a.py': {1:None, 2:None}, 'b.py': {3:None} } +SUMMARY_1 = { 'a.py':2, 'b.py':1 } +MEASURED_FILES_1 = [ 'a.py', 'b.py' ] +A_PY_LINES_1 = [1,2] +B_PY_LINES_1 = [3] + +DATA_2 = { 'a.py': {1:None, 5:None}, 'c.py': {17:None} } +SUMMARY_1_2 = { 'a.py':3, 'b.py':1, 'c.py':1 } +MEASURED_FILES_1_2 = [ 'a.py', 'b.py', 'c.py' ] + +ARC_DATA_3 = { 'x.py': {(1,2):None, (2,3):None}, 'y.py': {(17,23):None} } +X_PY_ARCS_3 = [(1,2), (2,3)] +Y_PY_ARCS_3 = [(17,23)] + + +class DataTest(CoverageTest): + """Test cases for coverage.data.""" + + def assert_summary(self, covdata, summary, fullpath=False): + """Check that the summary of `covdata` is `summary`.""" + self.assertEqual(covdata.summary(fullpath), summary) + + def assert_measured_files(self, covdata, measured): + """Check that `covdata`'s measured files are `measured`.""" + self.assertSameElements(covdata.measured_files(), measured) + + def test_reading_empty(self): + covdata = CoverageData() + covdata.read() + self.assert_summary(covdata, {}) + + def test_adding_data(self): + covdata = CoverageData() + covdata.add_line_data(DATA_1) + self.assert_summary(covdata, SUMMARY_1) + self.assert_measured_files(covdata, MEASURED_FILES_1) + + def test_touch_file(self): + covdata = CoverageData() + covdata.add_line_data(DATA_1) + covdata.touch_file('x.py') + self.assert_measured_files(covdata, MEASURED_FILES_1 + ['x.py']) + + def test_writing_and_reading(self): + covdata1 = CoverageData() + covdata1.add_line_data(DATA_1) + covdata1.write() + + covdata2 = CoverageData() + covdata2.read() + self.assert_summary(covdata2, SUMMARY_1) + + def test_combining(self): + covdata1 = CoverageData() + covdata1.add_line_data(DATA_1) + covdata1.write(suffix='1') + + covdata2 = CoverageData() + covdata2.add_line_data(DATA_2) + covdata2.write(suffix='2') + + covdata3 = CoverageData() + covdata3.combine_parallel_data() + self.assert_summary(covdata3, SUMMARY_1_2) + self.assert_measured_files(covdata3, MEASURED_FILES_1_2) + + def test_erasing(self): + covdata1 = CoverageData() + covdata1.add_line_data(DATA_1) + covdata1.write() + covdata1.erase() + self.assert_summary(covdata1, {}) + + covdata2 = CoverageData() + covdata2.read() + self.assert_summary(covdata2, {}) + + def test_file_format(self): + # Write with CoverageData, then read the pickle explicitly. + covdata = CoverageData() + covdata.add_line_data(DATA_1) + covdata.write() + + fdata = open(".coverage", 'rb') + try: + data = pickle.load(fdata) + finally: + fdata.close() + + lines = data['lines'] + self.assertSameElements(lines.keys(), MEASURED_FILES_1) + self.assertSameElements(lines['a.py'], A_PY_LINES_1) + self.assertSameElements(lines['b.py'], B_PY_LINES_1) + # If not measuring branches, there's no arcs entry. + self.assertEqual(data.get('arcs', 'not there'), 'not there') + + def test_file_format_with_arcs(self): + # Write with CoverageData, then read the pickle explicitly. + covdata = CoverageData() + covdata.add_arc_data(ARC_DATA_3) + covdata.write() + + fdata = open(".coverage", 'rb') + try: + data = pickle.load(fdata) + finally: + fdata.close() + + self.assertSameElements(data['lines'].keys(), []) + arcs = data['arcs'] + self.assertSameElements(arcs['x.py'], X_PY_ARCS_3) + self.assertSameElements(arcs['y.py'], Y_PY_ARCS_3) + + def test_combining_with_aliases(self): + covdata1 = CoverageData() + covdata1.add_line_data({ + '/home/ned/proj/src/a.py': {1:None, 2:None}, + '/home/ned/proj/src/sub/b.py': {3:None}, + }) + covdata1.write(suffix='1') + + covdata2 = CoverageData() + covdata2.add_line_data({ + r'c:\ned\test\a.py': {4:None, 5:None}, + r'c:\ned\test\sub\b.py': {6:None}, + }) + covdata2.write(suffix='2') + + covdata3 = CoverageData() + aliases = PathAliases() + aliases.add("/home/ned/proj/src/", "./") + aliases.add(r"c:\ned\test", "./") + covdata3.combine_parallel_data(aliases=aliases) + self.assert_summary( + covdata3, { './a.py':4, './sub/b.py':2 }, fullpath=True + ) + self.assert_measured_files(covdata3, [ './a.py', './sub/b.py' ]) diff --git a/test/test_execfile.py b/test/test_execfile.py new file mode 100644 index 0000000..e7d7041 --- /dev/null +++ b/test/test_execfile.py @@ -0,0 +1,116 @@ +"""Tests for coverage.execfile""" + +import os, sys + +from coverage.execfile import run_python_file, run_python_module +from coverage.misc import NoSource + +from test.coveragetest import CoverageTest + +here = os.path.dirname(__file__) + +class RunFileTest(CoverageTest): + """Test cases for `run_python_file`.""" + + def test_run_python_file(self): + tryfile = os.path.join(here, "try_execfile.py") + run_python_file(tryfile, [tryfile, "arg1", "arg2"]) + mod_globs = eval(self.stdout()) + + # The file should think it is __main__ + self.assertEqual(mod_globs['__name__'], "__main__") + + # It should seem to come from a file named try_execfile.py + dunder_file = os.path.basename(mod_globs['__file__']) + self.assertEqual(dunder_file, "try_execfile.py") + + # It should have its correct module data. + self.assertEqual(mod_globs['__doc__'], + "Test file for run_python_file.") + self.assertEqual(mod_globs['DATA'], "xyzzy") + self.assertEqual(mod_globs['FN_VAL'], "my_fn('fooey')") + + # It must be self-importable as __main__. + self.assertEqual(mod_globs['__main__.DATA'], "xyzzy") + + # Argv should have the proper values. + self.assertEqual(mod_globs['argv'], [tryfile, "arg1", "arg2"]) + + # __builtins__ should have the right values, like open(). + self.assertEqual(mod_globs['__builtins__.has_open'], True) + + def test_no_extra_file(self): + # Make sure that running a file doesn't create an extra compiled file. + self.make_file("xxx", """\ + desc = "a non-.py file!" + """) + + self.assertEqual(os.listdir("."), ["xxx"]) + run_python_file("xxx", ["xxx"]) + self.assertEqual(os.listdir("."), ["xxx"]) + + def test_universal_newlines(self): + # Make sure we can read any sort of line ending. + pylines = """# try newlines|print('Hello, world!')|""".split('|') + for nl in ('\n', '\r\n', '\r'): + fpy = open('nl.py', 'wb') + try: + fpy.write(nl.join(pylines).encode('utf-8')) + finally: + fpy.close() + run_python_file('nl.py', ['nl.py']) + self.assertEqual(self.stdout(), "Hello, world!\n"*3) + + def test_missing_final_newline(self): + # Make sure we can deal with a Python file with no final newline. + self.make_file("abrupt.py", """\ + if 1: + a = 1 + print("a is %r" % a) + #""") + abrupt = open("abrupt.py").read() + self.assertEqual(abrupt[-1], '#') + run_python_file("abrupt.py", ["abrupt.py"]) + self.assertEqual(self.stdout(), "a is 1\n") + + def test_no_such_file(self): + self.assertRaises(NoSource, run_python_file, "xyzzy.py", []) + + +class RunModuleTest(CoverageTest): + """Test run_python_module.""" + + run_in_temp_dir = False + + def setUp(self): + super(RunModuleTest, self).setUp() + # Parent class saves and restores sys.path, we can just modify it. + sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules')) + + def test_runmod1(self): + run_python_module("runmod1", ["runmod1", "hello"]) + self.assertEqual(self.stdout(), "runmod1: passed hello\n") + + def test_runmod2(self): + run_python_module("pkg1.runmod2", ["runmod2", "hello"]) + self.assertEqual(self.stdout(), "runmod2: passed hello\n") + + def test_runmod3(self): + run_python_module("pkg1.sub.runmod3", ["runmod3", "hello"]) + self.assertEqual(self.stdout(), "runmod3: passed hello\n") + + def test_pkg1_main(self): + run_python_module("pkg1", ["pkg1", "hello"]) + self.assertEqual(self.stdout(), "pkg1.__main__: passed hello\n") + + def test_pkg1_sub_main(self): + run_python_module("pkg1.sub", ["pkg1.sub", "hello"]) + self.assertEqual(self.stdout(), "pkg1.sub.__main__: passed hello\n") + + def test_no_such_module(self): + self.assertRaises(NoSource, run_python_module, "i_dont_exist", []) + self.assertRaises(NoSource, run_python_module, "i.dont_exist", []) + self.assertRaises(NoSource, run_python_module, "i.dont.exist", []) + + def test_no_main(self): + self.assertRaises(NoSource, run_python_module, "pkg2", ["pkg2", "hi"]) diff --git a/test/test_farm.py b/test/test_farm.py new file mode 100644 index 0000000..f25d610 --- /dev/null +++ b/test/test_farm.py @@ -0,0 +1,366 @@ +"""Run tests in the farm subdirectory. Designed for nose.""" + +import difflib, filecmp, fnmatch, glob, os, re, shutil, sys +from nose.plugins.skip import SkipTest + +from test.backtest import run_command, execfile # pylint: disable=W0622 + +from coverage.control import _TEST_NAME_FILE + + +def test_farm(clean_only=False): + """A test-generating function for nose to find and run.""" + for fname in glob.glob("test/farm/*/*.py"): + case = FarmTestCase(fname, clean_only) + yield (case,) + + +class FarmTestCase(object): + """A test case from the farm tree. + + Tests are short Python script files, often called run.py: + + copy("src", "out") + run(''' + coverage -x white.py + coverage -a white.py + ''', rundir="out") + compare("out", "gold", "*,cover") + clean("out") + + Verbs (copy, run, compare, clean) are methods in this class. FarmTestCase + has options to allow various uses of the test cases (normal execution, + cleaning-only, or run and leave the results for debugging). + + """ + def __init__(self, runpy, clean_only=False, dont_clean=False): + """Create a test case from a run.py file. + + `clean_only` means that only the clean() action is executed. + `dont_clean` means that the clean() action is not executed. + + """ + self.description = runpy + self.dir, self.runpy = os.path.split(runpy) + self.clean_only = clean_only + self.dont_clean = dont_clean + + def cd(self, newdir): + """Change the current directory, and return the old one.""" + cwd = os.getcwd() + os.chdir(newdir) + return cwd + + def addtopath(self, directory): + """Add `directory` to the path, and return the old path.""" + oldpath = sys.path[:] + if directory is not None: + sys.path.insert(0, directory) + return oldpath + + def restorepath(self, path): + """Restore the system path to `path`.""" + sys.path = path + + def __call__(self): + """Execute the test from the run.py file. + + """ + if _TEST_NAME_FILE: + f = open(_TEST_NAME_FILE, "w") + f.write(self.description.replace("/", "_")) + f.close() + + cwd = self.cd(self.dir) + + # Prepare a dictionary of globals for the run.py files to use. + fns = """ + copy run runfunc compare contains doesnt_contain clean skip + """.split() + if self.clean_only: + glo = dict([(fn, self.noop) for fn in fns]) + glo['clean'] = self.clean + else: + glo = dict([(fn, getattr(self, fn)) for fn in fns]) + if self.dont_clean: # pragma: not covered + glo['clean'] = self.noop + + old_mods = dict(sys.modules) + try: + execfile(self.runpy, glo) + finally: + self.cd(cwd) + # Remove any new modules imported during the test run. This lets us + # import the same source files for more than one test. + to_del = [m for m in sys.modules if m not in old_mods] + for m in to_del: + del sys.modules[m] + + def run_fully(self): # pragma: not covered + """Run as a full test case, with setUp and tearDown.""" + self.setUp() + try: + self() + finally: + self.tearDown() + + def fnmatch_list(self, files, file_pattern): + """Filter the list of `files` to only those that match `file_pattern`. + + If `file_pattern` is None, then return the entire list of files. + + Returns a list of the filtered files. + + """ + if file_pattern: + files = [f for f in files if fnmatch.fnmatch(f, file_pattern)] + return files + + def setUp(self): + """Test set up, run by nose before __call__.""" + + # Modules should be importable from the current directory. + self.old_syspath = sys.path[:] + sys.path.insert(0, '') + + def tearDown(self): + """Test tear down, run by nose after __call__.""" + # Make sure no matter what, the test is cleaned up. + if not self.dont_clean: # pragma: part covered + self.clean_only = True + self() + + # Restore the original sys.path + sys.path = self.old_syspath + + # Functions usable inside farm run.py files + + def noop(self, *args, **kwargs): + """A no-op function to stub out run, copy, etc, when only cleaning.""" + pass + + def copy(self, src, dst): + """Copy a directory.""" + + if os.path.exists(dst): + shutil.rmtree(dst) + shutil.copytree(src, dst) + + def run(self, cmds, rundir="src", outfile=None): + """Run a list of commands. + + `cmds` is a string, commands separated by newlines. + `rundir` is the directory in which to run the commands. + `outfile` is a filename to redirect stdout to. + + """ + cwd = self.cd(rundir) + if outfile: + fout = open(outfile, "a+") + try: + for cmd in cmds.split("\n"): + cmd = cmd.strip() + if not cmd: + continue + retcode, output = run_command(cmd) + print(output.rstrip()) + if outfile: + fout.write(output) + if retcode: + raise Exception("command exited abnormally") + finally: + if outfile: + fout.close() + self.cd(cwd) + + def runfunc(self, fn, rundir="src", addtopath=None): + """Run a function. + + `fn` is a callable. + `rundir` is the directory in which to run the function. + + """ + + cwd = self.cd(rundir) + oldpath = self.addtopath(addtopath) + try: + fn() + finally: + self.cd(cwd) + self.restorepath(oldpath) + + def compare(self, dir1, dir2, file_pattern=None, size_within=0, + left_extra=False, right_extra=False, scrubs=None + ): + """Compare files matching `file_pattern` in `dir1` and `dir2`. + + `dir2` is interpreted as a prefix, with Python version numbers appended + to find the actual directory to compare with. "foo" will compare against + "foo_v241", "foo_v24", "foo_v2", or "foo", depending on which directory + is found first. + + `size_within` is a percentage delta for the file sizes. If non-zero, + then the file contents are not compared (since they are expected to + often be different), but the file sizes must be within this amount. + For example, size_within=10 means that the two files' sizes must be + within 10 percent of each other to compare equal. + + `left_extra` true means the left directory can have extra files in it + without triggering an assertion. `right_extra` means the right + directory can. + + `scrubs` is a list of pairs, regex find and replace patterns to use to + scrub the files of unimportant differences. + + An assertion will be raised if the directories fail one of their + matches. + + """ + # Search for a dir2 with a version suffix. + version_suff = ''.join(map(str, sys.version_info[:3])) + while version_suff: + trydir = dir2 + '_v' + version_suff + if os.path.exists(trydir): + dir2 = trydir + break + version_suff = version_suff[:-1] + + assert os.path.exists(dir1), "Left directory missing: %s" % dir1 + assert os.path.exists(dir2), "Right directory missing: %s" % dir2 + + dc = filecmp.dircmp(dir1, dir2) + diff_files = self.fnmatch_list(dc.diff_files, file_pattern) + left_only = self.fnmatch_list(dc.left_only, file_pattern) + right_only = self.fnmatch_list(dc.right_only, file_pattern) + + if size_within: + # The files were already compared, use the diff_files list as a + # guide for size comparison. + wrong_size = [] + for f in diff_files: + left = open(os.path.join(dir1, f), "rb").read() + right = open(os.path.join(dir2, f), "rb").read() + size_l, size_r = len(left), len(right) + big, little = max(size_l, size_r), min(size_l, size_r) + if (big - little) / float(little) > size_within/100.0: + # print "%d %d" % (big, little) + # print "Left: ---\n%s\n-----\n%s" % (left, right) + wrong_size.append(f) + assert not wrong_size, ( + "File sizes differ between %s and %s: %s" % ( + dir1, dir2, wrong_size + )) + else: + # filecmp only compares in binary mode, but we want text mode. So + # look through the list of different files, and compare them + # ourselves. + text_diff = [] + for f in diff_files: + left = open(os.path.join(dir1, f), "rU").readlines() + right = open(os.path.join(dir2, f), "rU").readlines() + if scrubs: + left = self._scrub(left, scrubs) + right = self._scrub(right, scrubs) + if left != right: + text_diff.append(f) + print("".join(list(difflib.Differ().compare(left, right)))) + assert not text_diff, "Files differ: %s" % text_diff + + if not left_extra: + assert not left_only, "Files in %s only: %s" % (dir1, left_only) + if not right_extra: + assert not right_only, "Files in %s only: %s" % (dir2, right_only) + + def _scrub(self, strlist, scrubs): + """Scrub uninteresting data from the strings in `strlist`. + + `scrubs is a list of (find, replace) pairs of regexes that are used on + each string in `strlist`. A list of scrubbed strings is returned. + + """ + scrubbed = [] + for s in strlist: + for rgx_find, rgx_replace in scrubs: + s = re.sub(rgx_find, rgx_replace, s) + scrubbed.append(s) + return scrubbed + + def contains(self, filename, *strlist): + """Check that the file contains all of a list of strings. + + An assert will be raised if one of the arguments in `strlist` is + missing in `filename`. + + """ + text = open(filename, "r").read() + for s in strlist: + assert s in text, "Missing content in %s: %r" % (filename, s) + + def doesnt_contain(self, filename, *strlist): + """Check that the file contains none of a list of strings. + + An assert will be raised if any of the strings in strlist appears in + `filename`. + + """ + text = open(filename, "r").read() + for s in strlist: + assert s not in text, "Forbidden content in %s: %r" % (filename, s) + + def clean(self, cleandir): + """Clean `cleandir` by removing it and all its children completely.""" + # rmtree gives mysterious failures on Win7, so retry a "few" times. + # I've seen it take over 100 tries, so, 1000! This is probably the + # most unpleasant hack I've written in a long time... + tries = 1000 + while tries: # pragma: part covered + if os.path.exists(cleandir): + try: + shutil.rmtree(cleandir) + except OSError: # pragma: not covered + if tries == 1: + raise + else: + tries -= 1 + continue + break + + def skip(self, msg=None): + """Skip the current test.""" + raise SkipTest(msg) + + +def main(): # pragma: not covered + """Command-line access to test_farm. + + Commands: + + run testcase - Run a single test case. + out testcase - Run a test case, but don't clean up, to see the output. + clean - Clean all the output for all tests. + + """ + op = 'help' + try: + op = sys.argv[1] + except IndexError: + pass + + if op == 'run': + # Run the test for real. + case = FarmTestCase(sys.argv[2]) + case.run_fully() + elif op == 'out': + # Run the test, but don't clean up, so we can examine the output. + case = FarmTestCase(sys.argv[2], dont_clean=True) + case.run_fully() + elif op == 'clean': + # Run all the tests, but just clean. + for test in test_farm(clean_only=True): + test[0].run_fully() + else: + print(main.__doc__) + +# So that we can run just one farm run.py at a time. +if __name__ == '__main__': + main() diff --git a/test/test_files.py b/test/test_files.py new file mode 100644 index 0000000..051b274 --- /dev/null +++ b/test/test_files.py @@ -0,0 +1,169 @@ +"""Tests for files.py""" + +import os + +from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher +from coverage.files import PathAliases, find_python_files, abs_file +from coverage.backward import set # pylint: disable=W0622 +from coverage.misc import CoverageException + +from test.coveragetest import CoverageTest + + +class FileLocatorTest(CoverageTest): + """Tests of `FileLocator`.""" + + def abs_path(self, p): + """Return the absolute path for `p`.""" + return os.path.join(os.getcwd(), os.path.normpath(p)) + + def test_simple(self): + self.make_file("hello.py") + fl = FileLocator() + self.assertEqual(fl.relative_filename("hello.py"), "hello.py") + a = self.abs_path("hello.py") + self.assertNotEqual(a, "hello.py") + self.assertEqual(fl.relative_filename(a), "hello.py") + + def test_peer_directories(self): + self.make_file("sub/proj1/file1.py") + self.make_file("sub/proj2/file2.py") + a1 = self.abs_path("sub/proj1/file1.py") + a2 = self.abs_path("sub/proj2/file2.py") + d = os.path.normpath("sub/proj1") + os.chdir(d) + fl = FileLocator() + self.assertEqual(fl.relative_filename(a1), "file1.py") + self.assertEqual(fl.relative_filename(a2), a2) + + def test_filepath_contains_absolute_prefix_twice(self): + # https://bitbucket.org/ned/coveragepy/issue/194 + # Build a path that has two pieces matching the absolute path prefix. + # Technically, this test doesn't do that on Windows, but drive + # letters make that impractical to acheive. + fl = FileLocator() + d = abs_file(os.curdir) + trick = os.path.splitdrive(d)[1].lstrip(os.path.sep) + rel = os.path.join('sub', trick, 'file1.py') + self.assertEqual(fl.relative_filename(abs_file(rel)), rel) + + +class MatcherTest(CoverageTest): + """Tests of file matchers.""" + + def test_tree_matcher(self): + file1 = self.make_file("sub/file1.py") + file2 = self.make_file("sub/file2.c") + file3 = self.make_file("sub2/file3.h") + file4 = self.make_file("sub3/file4.py") + file5 = self.make_file("sub3/file5.c") + fl = FileLocator() + tm = TreeMatcher([ + fl.canonical_filename("sub"), + fl.canonical_filename(file4), + ]) + self.assertTrue(tm.match(fl.canonical_filename(file1))) + self.assertTrue(tm.match(fl.canonical_filename(file2))) + self.assertFalse(tm.match(fl.canonical_filename(file3))) + self.assertTrue(tm.match(fl.canonical_filename(file4))) + self.assertFalse(tm.match(fl.canonical_filename(file5))) + + def test_fnmatch_matcher(self): + file1 = self.make_file("sub/file1.py") + file2 = self.make_file("sub/file2.c") + file3 = self.make_file("sub2/file3.h") + file4 = self.make_file("sub3/file4.py") + file5 = self.make_file("sub3/file5.c") + fl = FileLocator() + fnm = FnmatchMatcher(["*.py", "*/sub2/*"]) + self.assertTrue(fnm.match(fl.canonical_filename(file1))) + self.assertFalse(fnm.match(fl.canonical_filename(file2))) + self.assertTrue(fnm.match(fl.canonical_filename(file3))) + self.assertTrue(fnm.match(fl.canonical_filename(file4))) + self.assertFalse(fnm.match(fl.canonical_filename(file5))) + + +class PathAliasesTest(CoverageTest): + """Tests for coverage/files.py:PathAliases""" + + def test_noop(self): + aliases = PathAliases() + self.assertEqual(aliases.map('/ned/home/a.py'), '/ned/home/a.py') + + def test_nomatch(self): + aliases = PathAliases() + aliases.add('/home/*/src', './mysrc') + self.assertEqual(aliases.map('/home/foo/a.py'), '/home/foo/a.py') + + def test_wildcard(self): + aliases = PathAliases() + aliases.add('/ned/home/*/src', './mysrc') + self.assertEqual(aliases.map('/ned/home/foo/src/a.py'), './mysrc/a.py') + aliases = PathAliases() + aliases.add('/ned/home/*/src/', './mysrc') + self.assertEqual(aliases.map('/ned/home/foo/src/a.py'), './mysrc/a.py') + + def test_no_accidental_match(self): + aliases = PathAliases() + aliases.add('/home/*/src', './mysrc') + self.assertEqual(aliases.map('/home/foo/srcetc'), '/home/foo/srcetc') + + def test_multiple_patterns(self): + aliases = PathAliases() + aliases.add('/home/*/src', './mysrc') + aliases.add('/lib/*/libsrc', './mylib') + self.assertEqual(aliases.map('/home/foo/src/a.py'), './mysrc/a.py') + self.assertEqual(aliases.map('/lib/foo/libsrc/a.py'), './mylib/a.py') + + def test_cant_have_wildcard_at_end(self): + aliases = PathAliases() + self.assertRaisesRegexp( + CoverageException, "Pattern must not end with wildcards.", + aliases.add, "/ned/home/*", "fooey" + ) + self.assertRaisesRegexp( + CoverageException, "Pattern must not end with wildcards.", + aliases.add, "/ned/home/*/", "fooey" + ) + self.assertRaisesRegexp( + CoverageException, "Pattern must not end with wildcards.", + aliases.add, "/ned/home/*/*/", "fooey" + ) + + def test_no_accidental_munging(self): + aliases = PathAliases() + aliases.add(r'c:\Zoo\boo', 'src/') + aliases.add('/home/ned$', 'src/') + self.assertEqual(aliases.map(r'c:\Zoo\boo\foo.py'), 'src/foo.py') + self.assertEqual(aliases.map(r'/home/ned$/foo.py'), 'src/foo.py') + + def test_paths_are_os_corrected(self): + aliases = PathAliases() + aliases.add('/home/ned/*/src', './mysrc') + aliases.add(r'c:\ned\src', './mysrc') + mapped = aliases.map(r'C:\Ned\src\sub\a.py') + self.assertEqual(mapped, './mysrc/sub/a.py') + + aliases = PathAliases() + aliases.add('/home/ned/*/src', r'.\mysrc') + aliases.add(r'c:\ned\src', r'.\mysrc') + mapped = aliases.map(r'/home/ned/foo/src/sub/a.py') + self.assertEqual(mapped, r'.\mysrc\sub\a.py') + + +class FindPythonFilesTest(CoverageTest): + """Tests of `find_python_files`.""" + + def test_find_python_files(self): + self.make_file("sub/a.py") + self.make_file("sub/b.py") + self.make_file("sub/x.c") # nope: not .py + self.make_file("sub/ssub/__init__.py") + self.make_file("sub/ssub/s.py") + self.make_file("sub/ssub/~s.py") # nope: editor effluvia + self.make_file("sub/lab/exp.py") # nope: no __init__.py + py_files = set(find_python_files("sub")) + self.assert_same_files(py_files, [ + "sub/a.py", "sub/b.py", + "sub/ssub/__init__.py", "sub/ssub/s.py", + ]) diff --git a/test/test_html.py b/test/test_html.py new file mode 100644 index 0000000..66d1bd7 --- /dev/null +++ b/test/test_html.py @@ -0,0 +1,297 @@ +# -*- coding: utf-8 -*- +"""Tests that HTML generation is awesome.""" + +import os.path, re, sys +import coverage +from coverage.misc import NotPython, NoSource + +from test.coveragetest import CoverageTest + +class HtmlTestHelpers(CoverageTest): + """Methods that help with HTML tests.""" + + def create_initial_files(self): + """Create the source files we need to run these tests.""" + self.make_file("main_file.py", """\ + import helper1, helper2 + helper1.func1(12) + helper2.func2(12) + """) + self.make_file("helper1.py", """\ + def func1(x): + if x % 2: + print("odd") + """) + self.make_file("helper2.py", """\ + def func2(x): + print("x is %d" % x) + """) + + def run_coverage(self, covargs=None, htmlargs=None): + """Run coverage on main_file.py, and create an HTML report.""" + self.clean_local_file_imports() + cov = coverage.coverage(**(covargs or {})) + self.start_import_stop(cov, "main_file") + cov.html_report(**(htmlargs or {})) + + def remove_html_files(self): + """Remove the HTML files created as part of the HTML report.""" + os.remove("htmlcov/index.html") + os.remove("htmlcov/main_file.html") + os.remove("htmlcov/helper1.html") + os.remove("htmlcov/helper2.html") + + +class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): + """Tests of the HTML delta speed-ups.""" + + def setUp(self): + super(HtmlDeltaTest, self).setUp() + + # At least one of our tests monkey-patches the version of coverage, + # so grab it here to restore it later. + self.real_coverage_version = coverage.__version__ + + self.maxDiff = None + + def tearDown(self): + coverage.__version__ = self.real_coverage_version + super(HtmlDeltaTest, self).tearDown() + + def test_html_created(self): + # Test basic HTML generation: files should be created. + self.create_initial_files() + self.run_coverage() + + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/main_file.html") + self.assert_exists("htmlcov/helper1.html") + self.assert_exists("htmlcov/helper2.html") + self.assert_exists("htmlcov/style.css") + self.assert_exists("htmlcov/coverage_html.js") + + def test_html_delta_from_source_change(self): + # HTML generation can create only the files that have changed. + # In this case, helper1 changes because its source is different. + self.create_initial_files() + self.run_coverage() + index1 = open("htmlcov/index.html").read() + self.remove_html_files() + + # Now change a file and do it again + self.make_file("helper1.py", """\ + def func1(x): # A nice function + if x % 2: + print("odd") + """) + + self.run_coverage() + + # Only the changed files should have been created. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1.html") + self.assert_doesnt_exist("htmlcov/main_file.html") + self.assert_doesnt_exist("htmlcov/helper2.html") + index2 = open("htmlcov/index.html").read() + self.assertMultiLineEqual(index1, index2) + + def test_html_delta_from_coverage_change(self): + # HTML generation can create only the files that have changed. + # In this case, helper1 changes because its coverage is different. + self.create_initial_files() + self.run_coverage() + self.remove_html_files() + + # Now change a file and do it again + self.make_file("main_file.py", """\ + import helper1, helper2 + helper1.func1(23) + helper2.func2(23) + """) + + self.run_coverage() + + # Only the changed files should have been created. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1.html") + self.assert_exists("htmlcov/main_file.html") + self.assert_doesnt_exist("htmlcov/helper2.html") + + def test_html_delta_from_settings_change(self): + # HTML generation can create only the files that have changed. + # In this case, everything changes because the coverage settings have + # changed. + self.create_initial_files() + self.run_coverage(covargs=dict(omit=[])) + index1 = open("htmlcov/index.html").read() + self.remove_html_files() + + self.run_coverage(covargs=dict(omit=['xyzzy*'])) + + # All the files have been reported again. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1.html") + self.assert_exists("htmlcov/main_file.html") + self.assert_exists("htmlcov/helper2.html") + index2 = open("htmlcov/index.html").read() + self.assertMultiLineEqual(index1, index2) + + def test_html_delta_from_coverage_version_change(self): + # HTML generation can create only the files that have changed. + # In this case, everything changes because the coverage version has + # changed. + self.create_initial_files() + self.run_coverage() + index1 = open("htmlcov/index.html").read() + self.remove_html_files() + + # "Upgrade" coverage.py! + coverage.__version__ = "XYZZY" + + self.run_coverage() + + # All the files have been reported again. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1.html") + self.assert_exists("htmlcov/main_file.html") + self.assert_exists("htmlcov/helper2.html") + index2 = open("htmlcov/index.html").read() + fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) + self.assertMultiLineEqual(index1, fixed_index2) + + +class HtmlTitleTests(HtmlTestHelpers, CoverageTest): + """Tests of the HTML title support.""" + + def test_default_title(self): + self.create_initial_files() + self.run_coverage() + index = open("htmlcov/index.html").read() + self.assertIn("Coverage report", index) + self.assertIn("

    Coverage report:", index) + + def test_title_set_in_config_file(self): + self.create_initial_files() + self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n") + self.run_coverage() + index = open("htmlcov/index.html").read() + self.assertIn("Metrics & stuff!", index) + self.assertIn("

    Metrics & stuff!:", index) + + if sys.version_info[:2] != (3,1): + def test_non_ascii_title_set_in_config_file(self): + self.create_initial_files() + self.make_file(".coveragerc", + "[html]\ntitle = «ταБЬℓσ» numbers" + ) + self.run_coverage() + index = open("htmlcov/index.html").read() + self.assertIn( + "«ταБЬℓσ»" + " numbers", index + ) + self.assertIn( + "<h1>«ταБЬℓσ»" + " numbers", index + ) + + def test_title_set_in_args(self): + self.create_initial_files() + self.make_file(".coveragerc", "[html]\ntitle = Good title\n") + self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!")) + index = open("htmlcov/index.html").read() + self.assertIn( + "<title>«ταБЬℓσ»" + " & stüff!", index + ) + self.assertIn( + "

    «ταБЬℓσ»" + " & stüff!:", index + ) + + +class HtmlWithUnparsableFilesTest(CoverageTest): + """Test the behavior when measuring unparsable files.""" + + def test_dotpy_not_python(self): + self.make_file("innocuous.py", "a = 1") + cov = coverage.coverage() + self.start_import_stop(cov, "innocuous") + self.make_file("innocuous.py", "

    This isn't python!

    ") + self.assertRaisesRegexp( + NotPython, + "Couldn't parse '.*innocuous.py' as Python source: '.*' at line 1", + cov.html_report + ) + + def test_dotpy_not_python_ignored(self): + self.make_file("innocuous.py", "a = 2") + cov = coverage.coverage() + self.start_import_stop(cov, "innocuous") + self.make_file("innocuous.py", "

    This isn't python!

    ") + cov.html_report(ignore_errors=True) + self.assert_exists("htmlcov/index.html") + # this would be better as a glob, if the html layout changes: + self.assert_doesnt_exist("htmlcov/innocuous.html") + + def test_dothtml_not_python(self): + # We run a .html file, and when reporting, we can't parse it as + # Python. Since it wasn't .py, no error is reported. + + # Run an "html" file + self.make_file("innocuous.html", "a = 3") + self.run_command("coverage run innocuous.html") + # Before reporting, change it to be an HTML file. + self.make_file("innocuous.html", "

    This isn't python at all!

    ") + output = self.run_command("coverage html") + self.assertEqual(output.strip(), "No data to report.") + + def test_execed_liar_ignored(self): + # Jinja2 sets __file__ to be a non-Python file, and then execs code. + # If that file contains non-Python code, a TokenError shouldn't + # have been raised when writing the HTML report. + if sys.version_info < (3, 0): + source = "exec compile('','','exec') in {'__file__': 'liar.html'}" + else: + source = "exec(compile('','','exec'), {'__file__': 'liar.html'})" + self.make_file("liar.py", source) + self.make_file("liar.html", "{# Whoops, not python code #}") + cov = coverage.coverage() + self.start_import_stop(cov, "liar") + cov.html_report() + self.assert_exists("htmlcov/index.html") + + def test_execed_liar_ignored_indentation_error(self): + # Jinja2 sets __file__ to be a non-Python file, and then execs code. + # If that file contains untokenizable code, we shouldn't get an + # exception. + if sys.version_info < (3, 0): + source = "exec compile('','','exec') in {'__file__': 'liar.html'}" + else: + source = "exec(compile('','','exec'), {'__file__': 'liar.html'})" + self.make_file("liar.py", source) + # Tokenize will raise an IndentationError if it can't dedent. + self.make_file("liar.html", "0\n 2\n 1\n") + cov = coverage.coverage() + self.start_import_stop(cov, "liar") + cov.html_report() + self.assert_exists("htmlcov/index.html") + + +class HtmlTest(CoverageTest): + """Moar HTML tests.""" + + def test_missing_source_file_incorrect_message(self): + # https://bitbucket.org/ned/coveragepy/issue/60 + self.make_file("thefile.py", "import sub.another\n") + self.make_file("sub/__init__.py", "") + self.make_file("sub/another.py", "print('another')\n") + cov = coverage.coverage() + self.start_import_stop(cov, 'thefile') + os.remove("sub/another.py") + + missing_file = os.path.join(self.temp_dir, "sub", "another.py") + self.assertRaisesRegexp(NoSource, + "(?i)No source for code: '%s'" % re.escape(missing_file), + cov.html_report + ) diff --git a/test/test_misc.py b/test/test_misc.py new file mode 100644 index 0000000..a32030c --- /dev/null +++ b/test/test_misc.py @@ -0,0 +1,73 @@ +"""Tests of miscellaneous stuff.""" + +from coverage.misc import Hasher, file_be_gone +from coverage import __version__, __url__ +from test.coveragetest import CoverageTest + +class HasherTest(CoverageTest): + """Test our wrapper of md5 hashing.""" + + def test_string_hashing(self): + h1 = Hasher() + h1.update("Hello, world!") + h2 = Hasher() + h2.update("Goodbye!") + h3 = Hasher() + h3.update("Hello, world!") + self.assertNotEqual(h1.digest(), h2.digest()) + self.assertEqual(h1.digest(), h3.digest()) + + def test_dict_hashing(self): + h1 = Hasher() + h1.update({'a': 17, 'b': 23}) + h2 = Hasher() + h2.update({'b': 23, 'a': 17}) + self.assertEqual(h1.digest(), h2.digest()) + + +class RemoveFileTest(CoverageTest): + """Tests of misc.file_be_gone.""" + + def test_remove_nonexistent_file(self): + # it's ok to try to remove a file that doesn't exist. + file_be_gone("not_here.txt") + + def test_remove_actual_file(self): + # it really does remove a file that does exist. + self.make_file("here.txt", "We are here, we are here, we are here!") + file_be_gone("here.txt") + self.assert_doesnt_exist("here.txt") + + def test_actual_errors(self): + # Errors can still happen. + # ". is a directory" on Unix, or "Access denied" on Windows + self.assertRaises(OSError, file_be_gone, ".") + + +class SetupPyTest(CoverageTest): + """Tests of setup.py""" + + run_in_temp_dir = False + + def test_metadata(self): + status, output = self.run_command_status( + "python setup.py --description --version --url --author" + ) + self.assertEqual(status, 0) + out = output.splitlines() + self.assertIn("measurement", out[0]) + self.assertEqual(out[1], __version__) + self.assertEqual(out[2], __url__) + self.assertIn("Ned Batchelder", out[3]) + + def test_more_metadata(self): + from setup import setup_args + + classifiers = setup_args['classifiers'] + self.assertGreater(len(classifiers), 7) + self.assertTrue(classifiers[-1].startswith("Development Status ::")) + + long_description = setup_args['long_description'].splitlines() + self.assertGreater(len(long_description), 7) + self.assertNotEqual(long_description[0].strip(), "") + self.assertNotEqual(long_description[-1].strip(), "") diff --git a/test/test_oddball.py b/test/test_oddball.py new file mode 100644 index 0000000..113328b --- /dev/null +++ b/test/test_oddball.py @@ -0,0 +1,386 @@ +"""Oddball cases for testing coverage.py""" + +import os, sys +import coverage + +from test.coveragetest import CoverageTest +from test import osinfo + +class ThreadingTest(CoverageTest): + """Tests of the threading support.""" + + def test_threading(self): + self.check_coverage("""\ + import threading + + def fromMainThread(): + return "called from main thread" + + def fromOtherThread(): + return "called from other thread" + + def neverCalled(): + return "no one calls me" + + other = threading.Thread(target=fromOtherThread) + other.start() + fromMainThread() + other.join() + """, + [1,3,4,6,7,9,10,12,13,14,15], "10") + + def test_thread_run(self): + self.check_coverage("""\ + import threading + + class TestThread(threading.Thread): + def run(self): + self.a = 5 + self.do_work() + self.a = 7 + + def do_work(self): + self.a = 10 + + thd = TestThread() + thd.start() + thd.join() + """, + [1,3,4,5,6,7,9,10,12,13,14], "") + + +class RecursionTest(CoverageTest): + """Check what happens when recursive code gets near limits.""" + + def test_short_recursion(self): + # We can definitely get close to 500 stack frames. + self.check_coverage("""\ + def recur(n): + if n == 0: + return 0 + else: + return recur(n-1)+1 + + recur(495) # We can get at least this many stack frames. + i = 8 # and this line will be traced + """, + [1,2,3,5,7,8], "") + + def test_long_recursion(self): + # We can't finish a very deep recursion, but we don't crash. + self.assertRaises(RuntimeError, self.check_coverage, + """\ + def recur(n): + if n == 0: + return 0 + else: + return recur(n-1)+1 + + recur(100000) # This is definitely too many frames. + """, + [1,2,3,5,7], "") + + def test_long_recursion_recovery(self): + # Test the core of bug 93: http://bitbucket.org/ned/coveragepy/issue/93 + # When recovering from a stack overflow, the Python trace function is + # disabled, but the C trace function is not. So if we're using a + # Python trace function, we won't trace anything after the stack + # overflow, and there should be a warning about it. If we're using + # the C trace function, only line 3 will be missing, and all else + # will be traced. + + self.make_file("recur.py", """\ + def recur(n): + if n == 0: + return 0 # never hit + else: + return recur(n-1)+1 + + try: + recur(100000) # This is definitely too many frames. + except RuntimeError: + i = 10 + i = 11 + """) + + cov = coverage.coverage() + self.start_import_stop(cov, "recur") + + pytrace = (cov.collector.tracer_name() == "PyTracer") + expected_missing = [3] + if pytrace: + expected_missing += [9,10,11] + + _, statements, missing, _ = cov.analysis("recur.py") + self.assertEqual(statements, [1,2,3,5,7,8,9,10,11]) + self.assertEqual(missing, expected_missing) + + # We can get a warning about the stackoverflow effect on the tracing + # function only if we have sys.gettrace + if pytrace and hasattr(sys, "gettrace"): + self.assertEqual(cov._warnings, + ["Trace function changed, measurement is likely wrong: None"] + ) + else: + self.assertEqual(cov._warnings, []) + + +class MemoryLeakTest(CoverageTest): + """Attempt the impossible: test that memory doesn't leak. + + Note: this test is truly unusual, and may fail unexpectedly. + In particular, it is known to fail on PyPy if test_oddball.py is run in + isolation: https://bitbucket.org/ned/coveragepy/issue/186 + + """ + + def test_for_leaks(self): + lines = list(range(301, 315)) + lines.remove(306) + # Ugly string mumbo jumbo to get 300 blank lines at the beginning.. + code = """\ + # blank line\n""" * 300 + """\ + def once(x): + if x % 100 == 0: + raise Exception("100!") + elif x % 2: + return 10 + else: + return 11 + i = 0 # Portable loop without alloc'ing memory. + while i < ITERS: + try: + once(i) + except: + pass + i += 1 + """ + ram_0 = osinfo.process_ram() + self.check_coverage(code.replace("ITERS", "10"), lines, "") + ram_1 = osinfo.process_ram() + self.check_coverage(code.replace("ITERS", "10000"), lines, "") + ram_2 = osinfo.process_ram() + ram_growth = (ram_2 - ram_1) - (ram_1 - ram_0) + self.assertTrue(ram_growth < 100000, "RAM grew by %d" % (ram_growth)) + + +class PyexpatTest(CoverageTest): + """Pyexpat screws up tracing. Make sure we've counter-defended properly.""" + + def test_pyexpat(self): + # pyexpat calls the trace function explicitly (inexplicably), and does + # it wrong for exceptions. Parsing a DOCTYPE for some reason throws + # an exception internally, and triggers its wrong behavior. This test + # checks that our fake PyTrace_RETURN hack in tracer.c works. It will + # also detect if the pyexpat bug is fixed unbeknownst to us, meaning + # we'd see two RETURNs where there should only be one. + + self.make_file("trydom.py", """\ + import xml.dom.minidom + + XML = '''\\ + + + ''' + + def foo(): + dom = xml.dom.minidom.parseString(XML) + assert len(dom.getElementsByTagName('child')) == 2 + a = 11 + + foo() + """) + + self.make_file("outer.py", "\n"*100 + "import trydom\na = 102\n") + + cov = coverage.coverage() + cov.erase() + + # Import the python file, executing it. + self.start_import_stop(cov, "outer") + + _, statements, missing, _ = cov.analysis("trydom.py") + self.assertEqual(statements, [1,3,8,9,10,11,13]) + self.assertEqual(missing, []) + + _, statements, missing, _ = cov.analysis("outer.py") + self.assertEqual(statements, [101,102]) + self.assertEqual(missing, []) + + +class ExceptionTest(CoverageTest): + """I suspect different versions of Python deal with exceptions differently + in the trace function. + """ + + def test_exception(self): + # Python 2.3's trace function doesn't get called with "return" if the + # scope is exiting due to an exception. This confounds our trace + # function which relies on scope announcements to track which files to + # trace. + # + # This test is designed to sniff this out. Each function in the call + # stack is in a different file, to try to trip up the tracer. Each + # file has active lines in a different range so we'll see if the lines + # get attributed to the wrong file. + + self.make_file("oops.py", """\ + def oops(args): + a = 2 + raise Exception("oops") + a = 4 + """) + + self.make_file("fly.py", "\n"*100 + """\ + def fly(calls): + a = 2 + calls[0](calls[1:]) + a = 4 + """) + + self.make_file("catch.py", "\n"*200 + """\ + def catch(calls): + try: + a = 3 + calls[0](calls[1:]) + a = 5 + except: + a = 7 + """) + + self.make_file("doit.py", "\n"*300 + """\ + def doit(calls): + try: + calls[0](calls[1:]) + except: + a = 5 + """) + + # Import all the modules before starting coverage, so the def lines + # won't be in all the results. + for mod in "oops fly catch doit".split(): + self.import_local_file(mod) + + # Each run nests the functions differently to get different + # combinations of catching exceptions and letting them fly. + runs = [ + ("doit fly oops", { + 'doit.py': [302,303,304,305], + 'fly.py': [102,103], + 'oops.py': [2,3], + }), + ("doit catch oops", { + 'doit.py': [302,303], + 'catch.py': [202,203,204,206,207], + 'oops.py': [2,3], + }), + ("doit fly catch oops", { + 'doit.py': [302,303], + 'fly.py': [102,103,104], + 'catch.py': [202,203,204,206,207], + 'oops.py': [2,3], + }), + ("doit catch fly oops", { + 'doit.py': [302,303], + 'catch.py': [202,203,204,206,207], + 'fly.py': [102,103], + 'oops.py': [2,3], + }), + ] + + for callnames, lines_expected in runs: + + # Make the list of functions we'll call for this test. + calls = [getattr(sys.modules[cn], cn) for cn in callnames.split()] + + cov = coverage.coverage() + cov.start() + # Call our list of functions: invoke the first, with the rest as + # an argument. + calls[0](calls[1:]) # pragma: nested + cov.stop() # pragma: nested + + # Clean the line data and compare to expected results. + # The filenames are absolute, so keep just the base. + cov._harvest_data() # private! sshhh... + lines = cov.data.line_data() + clean_lines = {} + for f, llist in lines.items(): + if f == __file__: + # ignore this file. + continue + clean_lines[os.path.basename(f)] = llist + self.assertEqual(clean_lines, lines_expected) + + +if sys.version_info >= (2, 5): + class DoctestTest(CoverageTest): + """Tests invoked with doctest should measure properly.""" + + def setUp(self): + super(DoctestTest, self).setUp() + + # Oh, the irony! This test case exists because Python 2.4's + # doctest module doesn't play well with coverage. But nose fixes + # the problem by monkeypatching doctest. I want to undo the + # monkeypatch to be sure I'm getting the doctest module that users + # of coverage will get. Deleting the imported module here is + # enough: when the test imports doctest again, it will get a fresh + # copy without the monkeypatch. + del sys.modules['doctest'] + + def test_doctest(self): + self.check_coverage('''\ + def return_arg_or_void(arg): + """If is None, return "Void"; otherwise return + + >>> return_arg_or_void(None) + 'Void' + >>> return_arg_or_void("arg") + 'arg' + >>> return_arg_or_void("None") + 'None' + """ + if arg is None: + return "Void" + else: + return arg + + import doctest, sys + doctest.testmod(sys.modules[__name__]) # we're not __main__ :( + ''', + [1,11,12,14,16,17], "") + + +if hasattr(sys, 'gettrace'): + class GettraceTest(CoverageTest): + """Tests that we work properly with `sys.gettrace()`.""" + def test_round_trip(self): + self.check_coverage('''\ + import sys + def foo(n): + return 3*n + def bar(n): + return 5*n + a = foo(6) + sys.settrace(sys.gettrace()) + a = bar(8) + ''', + [1,2,3,4,5,6,7,8], "") + + def test_multi_layers(self): + self.check_coverage('''\ + import sys + def level1(): + a = 3 + level2() + b = 5 + def level2(): + c = 7 + sys.settrace(sys.gettrace()) + d = 9 + e = 10 + level1() + f = 12 + ''', + [1,2,3,4,5,6,7,8,9,10,11,12], "") diff --git a/test/test_parser.py b/test/test_parser.py new file mode 100644 index 0000000..6ccef20 --- /dev/null +++ b/test/test_parser.py @@ -0,0 +1,131 @@ +"""Tests for Coverage.py's code parsing.""" + +import textwrap +from test.coveragetest import CoverageTest +from coverage.parser import CodeParser + + +class ParserTest(CoverageTest): + """Tests for Coverage.py's code parsing.""" + + run_in_temp_dir = False + + def parse_source(self, text): + """Parse `text` as source, and return the `CodeParser` used.""" + text = textwrap.dedent(text) + cp = CodeParser(text=text, exclude="nocover") + cp.parse_source() + return cp + + def test_exit_counts(self): + cp = self.parse_source("""\ + # check some basic branch counting + class Foo: + def foo(self, a): + if a: + return 5 + else: + return 7 + + class Bar: + pass + """) + self.assertEqual(cp.exit_counts(), { + 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 + }) + + def test_try_except(self): + cp = self.parse_source("""\ + try: + a = 2 + except ValueError: + a = 4 + except ZeroDivideError: + a = 6 + except: + a = 8 + b = 9 + """) + self.assertEqual(cp.exit_counts(), { + 1: 1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 + }) + + def test_excluded_classes(self): + cp = self.parse_source("""\ + class Foo: + def __init__(self): + pass + + if 0: # nocover + class Bar: + pass + """) + self.assertEqual(cp.exit_counts(), { + 1:0, 2:1, 3:1 + }) + + def test_missing_branch_to_excluded_code(self): + cp = self.parse_source("""\ + if fooey: + a = 2 + else: # nocover + a = 4 + b = 5 + """) + self.assertEqual(cp.exit_counts(), { 1:1, 2:1, 5:1 }) + cp = self.parse_source("""\ + def foo(): + if fooey: + a = 3 + else: + a = 5 + b = 6 + """) + self.assertEqual(cp.exit_counts(), { 1:1, 2:2, 3:1, 5:1, 6:1 }) + cp = self.parse_source("""\ + def foo(): + if fooey: + a = 3 + else: # nocover + a = 5 + b = 6 + """) + self.assertEqual(cp.exit_counts(), { 1:1, 2:1, 3:1, 6:1 }) + + +class ParserFileTest(CoverageTest): + """Tests for Coverage.py's code parsing from files.""" + + def parse_file(self, filename): + """Parse `text` as source, and return the `CodeParser` used.""" + cp = CodeParser(filename=filename, exclude="nocover") + cp.parse_source() + return cp + + def test_line_endings(self): + text = """\ + # check some basic branch counting + class Foo: + def foo(self, a): + if a: + return 5 + else: + return 7 + + class Bar: + pass + """ + counts = { 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 } + name_endings = (("unix", "\n"), ("dos", "\r\n"), ("mac", "\r")) + for fname, newline in name_endings: + fname = fname + ".py" + self.make_file(fname, text, newline=newline) + cp = self.parse_file(fname) + self.assertEqual(cp.exit_counts(), counts) + + def test_encoding(self): + self.make_file("encoded.py", """\ + coverage = "\xe7\xf6v\xear\xe3g\xe9" + """) + cp = self.parse_file("encoded.py") + cp.exit_counts() diff --git a/test/test_phystokens.py b/test/test_phystokens.py new file mode 100644 index 0000000..e4834e4 --- /dev/null +++ b/test/test_phystokens.py @@ -0,0 +1,79 @@ +"""Tests for Coverage.py's improved tokenizer.""" + +import os, re +from test.coveragetest import CoverageTest +from coverage.phystokens import source_token_lines + + +SIMPLE = """\ +# yay! +def foo(): + say('two = %d' % 2) +""" + +MIXED_WS = """\ +def hello(): + a="Hello world!" +\tb="indented" +""" + +HERE = os.path.split(__file__)[0] + + +class PhysTokensTest(CoverageTest): + """Tests for Coverage.py's improver tokenizer.""" + + run_in_temp_dir = False + + def check_tokenization(self, source): + """Tokenize `source`, then put it back together, should be the same.""" + tokenized = "" + for line in source_token_lines(source): + text = "".join([t for _,t in line]) + tokenized += text + "\n" + # source_token_lines doesn't preserve trailing spaces, so trim all that + # before comparing. + source = source.replace('\r\n', '\n') + source = re.sub(r"(?m)[ \t]+$", "", source) + tokenized = re.sub(r"(?m)[ \t]+$", "", tokenized) + self.assertMultiLineEqual(source, tokenized) + + def check_file_tokenization(self, fname): + """Use the contents of `fname` for `check_tokenization`.""" + self.check_tokenization(open(fname).read()) + + def test_simple(self): + self.assertEqual(list(source_token_lines(SIMPLE)), + [ + [('com', "# yay!")], + [('key', 'def'), ('ws', ' '), ('nam', 'foo'), ('op', '('), + ('op', ')'), ('op', ':')], + [('ws', ' '), ('nam', 'say'), ('op', '('), + ('str', "'two = %d'"), ('ws', ' '), ('op', '%'), + ('ws', ' '), ('num', '2'), ('op', ')')] + ]) + self.check_tokenization(SIMPLE) + + def test_tab_indentation(self): + # Mixed tabs and spaces... + self.assertEqual(list(source_token_lines(MIXED_WS)), + [ + [('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), + ('op', ')'), ('op', ':')], + [('ws', ' '), ('nam', 'a'), ('op', '='), + ('str', '"Hello world!"')], + [('ws', ' '), ('nam', 'b'), ('op', '='), + ('str', '"indented"')], + ]) + + def test_tokenize_real_file(self): + # Check the tokenization of a real file (large, btw). + real_file = os.path.join(HERE, "test_coverage.py") + self.check_file_tokenization(real_file) + + def test_stress(self): + # Check the tokenization of a stress-test file. + stress = os.path.join(HERE, "stress_phystoken.tok") + self.check_file_tokenization(stress) + stress = os.path.join(HERE, "stress_phystoken_dos.tok") + self.check_file_tokenization(stress) diff --git a/test/test_process.py b/test/test_process.py new file mode 100644 index 0000000..bf22cc9 --- /dev/null +++ b/test/test_process.py @@ -0,0 +1,575 @@ +"""Tests for process behavior of coverage.py.""" + +import glob, os, sys, textwrap +from nose.plugins.skip import SkipTest +import coverage + +from test.coveragetest import CoverageTest + +here = os.path.dirname(__file__) + +class ProcessTest(CoverageTest): + """Tests of the per-process behavior of coverage.py.""" + + def number_of_data_files(self): + """Return the number of coverage data files in this directory.""" + num = 0 + for f in os.listdir('.'): + if f.startswith('.coverage.') or f == '.coverage': + num += 1 + return num + + def test_save_on_exit(self): + self.make_file("mycode.py", """\ + h = "Hello" + w = "world" + """) + + self.assert_doesnt_exist(".coverage") + self.run_command("coverage -x mycode.py") + self.assert_exists(".coverage") + + def test_environment(self): + # Checks that we can import modules from the test directory at all! + self.make_file("mycode.py", """\ + import covmod1 + import covmodzip1 + a = 1 + print ('done') + """) + + self.assert_doesnt_exist(".coverage") + out = self.run_command("coverage -x mycode.py") + self.assert_exists(".coverage") + self.assertEqual(out, 'done\n') + + def test_combine_parallel_data(self): + self.make_file("b_or_c.py", """\ + import sys + a = 1 + if sys.argv[1] == 'b': + b = 1 + else: + c = 1 + d = 1 + print ('done') + """) + + out = self.run_command("coverage -x -p b_or_c.py b") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + + out = self.run_command("coverage -x -p b_or_c.py c") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + + # After two -p runs, there should be two .coverage.machine.123 files. + self.assertEqual(self.number_of_data_files(), 2) + + # Combine the parallel coverage data files into .coverage . + self.run_command("coverage -c") + self.assert_exists(".coverage") + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage file and see that b_or_c.py has all 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.summary()['b_or_c.py'], 7) + + def test_combine_parallel_data_in_two_steps(self): + self.make_file("b_or_c.py", """\ + import sys + a = 1 + if sys.argv[1] == 'b': + b = 1 + else: + c = 1 + d = 1 + print ('done') + """) + + out = self.run_command("coverage -x -p b_or_c.py b") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + self.assertEqual(self.number_of_data_files(), 1) + + # Combine the (one) parallel coverage data file into .coverage . + self.run_command("coverage -c") + self.assert_exists(".coverage") + self.assertEqual(self.number_of_data_files(), 1) + + out = self.run_command("coverage -x -p b_or_c.py c") + self.assertEqual(out, 'done\n') + self.assert_exists(".coverage") + self.assertEqual(self.number_of_data_files(), 2) + + # Combine the parallel coverage data files into .coverage . + self.run_command("coverage -c") + self.assert_exists(".coverage") + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage file and see that b_or_c.py has all 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.summary()['b_or_c.py'], 7) + + def test_combine_with_rc(self): + self.make_file("b_or_c.py", """\ + import sys + a = 1 + if sys.argv[1] == 'b': + b = 1 + else: + c = 1 + d = 1 + print ('done') + """) + + self.make_file(".coveragerc", """\ + [run] + parallel = true + """) + + out = self.run_command("coverage run b_or_c.py b") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + + out = self.run_command("coverage run b_or_c.py c") + self.assertEqual(out, 'done\n') + self.assert_doesnt_exist(".coverage") + + # After two runs, there should be two .coverage.machine.123 files. + self.assertEqual(self.number_of_data_files(), 2) + + # Combine the parallel coverage data files into .coverage . + self.run_command("coverage combine") + self.assert_exists(".coverage") + self.assert_exists(".coveragerc") + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage file and see that b_or_c.py has all 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.summary()['b_or_c.py'], 7) + + # Reporting should still work even with the .rc file + out = self.run_command("coverage report") + self.assertMultiLineEqual(out, textwrap.dedent("""\ + Name Stmts Miss Cover + ---------------------------- + b_or_c 7 0 100% + """)) + + def test_combine_with_aliases(self): + self.make_file("d1/x.py", """\ + a = 1 + b = 2 + print("%s %s" % (a, b)) + """) + + self.make_file("d2/x.py", """\ + # 1 + # 2 + # 3 + c = 4 + d = 5 + print("%s %s" % (c, d)) + """) + + self.make_file(".coveragerc", """\ + [run] + parallel = True + + [paths] + source = + src + */d1 + */d2 + """) + + out = self.run_command("coverage run " + os.path.normpath("d1/x.py")) + self.assertEqual(out, '1 2\n') + out = self.run_command("coverage run " + os.path.normpath("d2/x.py")) + self.assertEqual(out, '4 5\n') + + self.assertEqual(self.number_of_data_files(), 2) + + self.run_command("coverage combine") + self.assert_exists(".coverage") + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage data file and see that the two different x.py + # files have been combined together. + data = coverage.CoverageData() + data.read_file(".coverage") + summary = data.summary(fullpath=True) + self.assertEqual(len(summary), 1) + actual = os.path.normcase(os.path.abspath(list(summary.keys())[0])) + expected = os.path.normcase(os.path.abspath('src/x.py')) + self.assertEqual(actual, expected) + self.assertEqual(list(summary.values())[0], 6) + + def test_missing_source_file(self): + # Check what happens if the source is missing when reporting happens. + self.make_file("fleeting.py", """\ + s = 'goodbye, cruel world!' + """) + + self.run_command("coverage run fleeting.py") + os.remove("fleeting.py") + out = self.run_command("coverage html -d htmlcov") + self.assertRegexpMatches(out, "No source for code: '.*fleeting.py'") + self.assertNotIn("Traceback", out) + + # It happens that the code paths are different for *.py and other + # files, so try again with no extension. + self.make_file("fleeting", """\ + s = 'goodbye, cruel world!' + """) + + self.run_command("coverage run fleeting") + os.remove("fleeting") + status, out = self.run_command_status("coverage html -d htmlcov", 1) + self.assertRegexpMatches(out, "No source for code: '.*fleeting'") + self.assertNotIn("Traceback", out) + self.assertEqual(status, 1) + + def test_running_missing_file(self): + status, out = self.run_command_status("coverage run xyzzy.py", 1) + self.assertRegexpMatches(out, "No file to run: .*xyzzy.py") + self.assertNotIn("raceback", out) + self.assertNotIn("rror", out) + self.assertEqual(status, 1) + + def test_code_throws(self): + self.make_file("throw.py", """\ + def f1(): + raise Exception("hey!") + + def f2(): + f1() + + f2() + """) + + # The important thing is for "coverage run" and "python" to report the + # same traceback. + status, out = self.run_command_status("coverage run throw.py", 1) + out2 = self.run_command("python throw.py") + if '__pypy__' in sys.builtin_module_names: + # Pypy has an extra frame in the traceback for some reason + lines2 = out2.splitlines() + out2 = "".join([l+"\n" for l in lines2 if "toplevel" not in l]) + self.assertMultiLineEqual(out, out2) + + # But also make sure that the output is what we expect. + self.assertIn('File "throw.py", line 5, in f2', out) + self.assertIn('raise Exception("hey!")', out) + self.assertNotIn('coverage', out) + self.assertEqual(status, 1) + + def test_code_exits(self): + self.make_file("exit.py", """\ + import sys + def f1(): + print("about to exit..") + sys.exit(17) + + def f2(): + f1() + + f2() + """) + + # The important thing is for "coverage run" and "python" to have the + # same output. No traceback. + status, out = self.run_command_status("coverage run exit.py", 17) + status2, out2 = self.run_command_status("python exit.py", 17) + self.assertMultiLineEqual(out, out2) + self.assertMultiLineEqual(out, "about to exit..\n") + self.assertEqual(status, status2) + self.assertEqual(status, 17) + + def test_code_exits_no_arg(self): + self.make_file("exit_none.py", """\ + import sys + def f1(): + print("about to exit quietly..") + sys.exit() + + f1() + """) + status, out = self.run_command_status("coverage run exit_none.py", 0) + status2, out2 = self.run_command_status("python exit_none.py", 0) + self.assertMultiLineEqual(out, out2) + self.assertMultiLineEqual(out, "about to exit quietly..\n") + self.assertEqual(status, status2) + self.assertEqual(status, 0) + + def test_coverage_run_is_like_python(self): + tryfile = os.path.join(here, "try_execfile.py") + self.make_file("run_me.py", open(tryfile).read()) + out = self.run_command("coverage run run_me.py") + out2 = self.run_command("python run_me.py") + self.assertMultiLineEqual(out, out2) + + if sys.version_info >= (2, 6): # Doesn't work in 2.5, and I don't care! + def test_coverage_run_dashm_is_like_python_dashm(self): + # These -m commands assume the coverage tree is on the path. + out = self.run_command("coverage run -m test.try_execfile") + out2 = self.run_command("python -m test.try_execfile") + self.assertMultiLineEqual(out, out2) + + if 0: # Expected failure + # For https://bitbucket.org/ned/coveragepy/issue/207 + def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): + self.make_file("package/__init__.py") # empty + self.make_file("package/__main__.py", "#\n") # empty + out = self.run_command("coverage run -m package") + out2 = self.run_command("python -m package") + self.assertMultiLineEqual(out, out2) + + if hasattr(os, 'fork'): + def test_fork(self): + self.make_file("fork.py", """\ + import os + + def child(): + print('Child!') + + def main(): + ret = os.fork() + + if ret == 0: + child() + else: + os.waitpid(ret, 0) + + main() + """) + + out = self.run_command("coverage run -p fork.py") + self.assertEqual(out, 'Child!\n') + self.assert_doesnt_exist(".coverage") + + # After running the forking program, there should be two + # .coverage.machine.123 files. + self.assertEqual(self.number_of_data_files(), 2) + + # Combine the parallel coverage data files into .coverage . + self.run_command("coverage -c") + self.assert_exists(".coverage") + + # After combining, there should be only the .coverage file. + self.assertEqual(self.number_of_data_files(), 1) + + # Read the coverage file and see that b_or_c.py has all 7 lines + # executed. + data = coverage.CoverageData() + data.read_file(".coverage") + self.assertEqual(data.summary()['fork.py'], 9) + + def test_warnings(self): + self.make_file("hello.py", """\ + import sys, os + print("Hello") + """) + out = self.run_command("coverage run --source=sys,xyzzy,quux hello.py") + + self.assertIn("Hello\n", out) + self.assertIn(textwrap.dedent("""\ + Coverage.py warning: Module sys has no Python source. + Coverage.py warning: Module xyzzy was never imported. + Coverage.py warning: Module quux was never imported. + Coverage.py warning: No data was collected. + """), out) + + def test_warnings_during_reporting(self): + # While fixing issue #224, the warnings were being printed far too + # often. Make sure they're not any more. + self.make_file("hello.py", """\ + import sys, os, the_other + print("Hello") + """) + self.make_file("the_other.py", """\ + print("What?") + """) + self.make_file(".coveragerc", """\ + [run] + source = + . + xyzzy + """) + + self.run_command("coverage run hello.py") + out = self.run_command("coverage html") + self.assertEqual(out.count("Module xyzzy was never imported."), 0) + + def test_warnings_if_never_run(self): + out = self.run_command("coverage run i_dont_exist.py") + self.assertIn("No file to run: 'i_dont_exist.py'", out) + self.assertNotIn("warning", out) + self.assertNotIn("Exception", out) + + out = self.run_command("coverage run -m no_such_module") + self.assertTrue( + ("No module named no_such_module" in out) or + ("No module named 'no_such_module'" in out) + ) + self.assertNotIn("warning", out) + self.assertNotIn("Exception", out) + + if sys.version_info >= (3, 0): # This only works on 3.x for now. + # It only works with the C tracer, + c_tracer = os.getenv('COVERAGE_TEST_TRACER', 'c') == 'c' + # and if we aren't measuring ourselves. + metacov = os.getenv('COVERAGE_COVERAGE', '') != '' + if c_tracer and not metacov: # pragma: not covered + def test_fullcoverage(self): + # fullcoverage is a trick to get stdlib modules measured from + # the very beginning of the process. Here we import os and + # then check how many lines are measured. + self.make_file("getenv.py", """\ + import os + print("FOOEY == %s" % os.getenv("FOOEY")) + """) + + fullcov = os.path.join( + os.path.dirname(coverage.__file__), "fullcoverage" + ) + self.set_environ("FOOEY", "BOO") + self.set_environ("PYTHONPATH", fullcov) + out = self.run_command("python -m coverage run -L getenv.py") + self.assertEqual(out, "FOOEY == BOO\n") + data = coverage.CoverageData() + data.read_file(".coverage") + # The actual number of executed lines in os.py when it's + # imported is 120 or so. Just running os.getenv executes + # about 5. + self.assertGreater(data.summary()['os.py'], 50) + + +class AliasedCommandTests(CoverageTest): + """Tests of the version-specific command aliases.""" + + def test_major_version_works(self): + # "coverage2" works on py2 + cmd = "coverage%d" % sys.version_info[0] + out = self.run_command(cmd) + self.assertIn("Code coverage for Python", out) + + def test_wrong_alias_doesnt_work(self): + # "coverage3" doesn't work on py2 + badcmd = "coverage%d" % (5 - sys.version_info[0]) + out = self.run_command(badcmd) + self.assertNotIn("Code coverage for Python", out) + + def test_specific_alias_works(self): + # "coverage-2.7" works on py2.7 + cmd = "coverage-%d.%d" % sys.version_info[:2] + out = self.run_command(cmd) + self.assertIn("Code coverage for Python", out) + + +class FailUnderTest(CoverageTest): + """Tests of the --fail-under switch.""" + + def setUp(self): + super(FailUnderTest, self).setUp() + self.make_file("fifty.py", """\ + # I have 50% coverage! + a = 1 + if a > 2: + b = 3 + c = 4 + """) + st, _ = self.run_command_status("coverage run fifty.py", 0) + self.assertEqual(st, 0) + + def test_report(self): + st, _ = self.run_command_status("coverage report --fail-under=50", 0) + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage report --fail-under=51", 2) + self.assertEqual(st, 2) + + def test_html_report(self): + st, _ = self.run_command_status("coverage html --fail-under=50", 0) + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage html --fail-under=51", 2) + self.assertEqual(st, 2) + + def test_xml_report(self): + st, _ = self.run_command_status("coverage xml --fail-under=50", 0) + self.assertEqual(st, 0) + st, _ = self.run_command_status("coverage xml --fail-under=51", 2) + self.assertEqual(st, 2) + + +class ProcessStartupTest(CoverageTest): + """Test that we can measure coverage in subprocesses.""" + + def setUp(self): + super(ProcessStartupTest, self).setUp() + # Find a place to put a .pth file. + pth_contents = "import coverage; coverage.process_startup()\n" + for d in sys.path: # pragma: part covered + g = glob.glob(os.path.join(d, "*.pth")) + if g: + pth_path = os.path.join(d, "subcover.pth") + pth = open(pth_path, "w") + try: + try: + pth.write(pth_contents) + self.pth_path = pth_path + break + except (IOError, OSError): # pragma: not covered + pass + finally: + pth.close() + else: # pragma: not covered + raise Exception("Couldn't find a place for the .pth file") + + def tearDown(self): + super(ProcessStartupTest, self).tearDown() + # Clean up the .pth file we made. + os.remove(self.pth_path) + + def test_subprocess_with_pth_files(self): # pragma: not covered + if os.environ.get('COVERAGE_COVERAGE', ''): + raise SkipTest( + "Can't test subprocess pth file suppport during metacoverage" + ) + # Main will run sub.py + self.make_file("main.py", """\ + import os + os.system("python sub.py") + """) + # sub.py will write a few lines. + self.make_file("sub.py", """\ + f = open("out.txt", "w") + f.write("Hello, world!\\n") + f.close() + """) + self.make_file("coverage.ini", """\ + [run] + data_file = .mycovdata + """) + self.set_environ("COVERAGE_PROCESS_START", "coverage.ini") + import main # pylint: disable=F0401,W0612 + + self.assertEqual(open("out.txt").read(), "Hello, world!\n") + # Read the data from .coverage + data = coverage.CoverageData() + data.read_file(".mycovdata") + self.assertEqual(data.summary()['sub.py'], 3) diff --git a/test/test_results.py b/test/test_results.py new file mode 100644 index 0000000..3caa5a6 --- /dev/null +++ b/test/test_results.py @@ -0,0 +1,60 @@ +"""Tests for Coverage.py's results analysis.""" + +from coverage.results import Numbers +from test.coveragetest import CoverageTest + + +class NumbersTest(CoverageTest): + """Tests for Coverage.py's numeric measurement summaries.""" + + run_in_temp_dir = False + + def test_basic(self): + n1 = Numbers(n_files=1, n_statements=200, n_missing=20) + self.assertEqual(n1.n_statements, 200) + self.assertEqual(n1.n_executed, 180) + self.assertEqual(n1.n_missing, 20) + self.assertEqual(n1.pc_covered, 90) + + def test_addition(self): + n1 = Numbers(n_files=1, n_statements=200, n_missing=20) + n2 = Numbers(n_files=1, n_statements=10, n_missing=8) + n3 = n1 + n2 + self.assertEqual(n3.n_files, 2) + self.assertEqual(n3.n_statements, 210) + self.assertEqual(n3.n_executed, 182) + self.assertEqual(n3.n_missing, 28) + self.assertAlmostEqual(n3.pc_covered, 86.666666666) + + def test_sum(self): + n1 = Numbers(n_files=1, n_statements=200, n_missing=20) + n2 = Numbers(n_files=1, n_statements=10, n_missing=8) + n3 = sum([n1, n2]) + self.assertEqual(n3.n_files, 2) + self.assertEqual(n3.n_statements, 210) + self.assertEqual(n3.n_executed, 182) + self.assertEqual(n3.n_missing, 28) + self.assertAlmostEqual(n3.pc_covered, 86.666666666) + + def test_pc_covered_str(self): + n0 = Numbers(n_files=1, n_statements=1000, n_missing=0) + n1 = Numbers(n_files=1, n_statements=1000, n_missing=1) + n999 = Numbers(n_files=1, n_statements=1000, n_missing=999) + n1000 = Numbers(n_files=1, n_statements=1000, n_missing=1000) + self.assertEqual(n0.pc_covered_str, "100") + self.assertEqual(n1.pc_covered_str, "99") + self.assertEqual(n999.pc_covered_str, "1") + self.assertEqual(n1000.pc_covered_str, "0") + + def test_pc_covered_str_precision(self): + assert Numbers._precision == 0 + Numbers.set_precision(1) + n0 = Numbers(n_files=1, n_statements=10000, n_missing=0) + n1 = Numbers(n_files=1, n_statements=10000, n_missing=1) + n9999 = Numbers(n_files=1, n_statements=10000, n_missing=9999) + n10000 = Numbers(n_files=1, n_statements=10000, n_missing=10000) + self.assertEqual(n0.pc_covered_str, "100.0") + self.assertEqual(n1.pc_covered_str, "99.9") + self.assertEqual(n9999.pc_covered_str, "0.1") + self.assertEqual(n10000.pc_covered_str, "0.0") + Numbers.set_precision(0) diff --git a/test/test_summary.py b/test/test_summary.py new file mode 100644 index 0000000..5bb903a --- /dev/null +++ b/test/test_summary.py @@ -0,0 +1,298 @@ +"""Test text-based summary reporting for coverage.py""" + +import os, re, sys + +import coverage +from coverage.backward import StringIO + +from test.coveragetest import CoverageTest + +class SummaryTest(CoverageTest): + """Tests of the text summary reporting for coverage.py.""" + + def setUp(self): + super(SummaryTest, self).setUp() + self.make_file("mycode.py", """\ + import covmod1 + import covmodzip1 + a = 1 + print ('done') + """) + # Parent class saves and restores sys.path, we can just modify it. + sys.path.append(self.nice_file(os.path.dirname(__file__), 'modules')) + + def report_from_command(self, cmd): + """Return the report from the `cmd`, with some convenience added.""" + report = self.run_command(cmd).replace('\\', '/') + self.assertNotIn("error", report.lower()) + return report + + def line_count(self, report): + """How many lines are in `report`?""" + self.assertEqual(report.split('\n')[-1], "") + return len(report.split('\n')) - 1 + + def last_line_squeezed(self, report): + """Return the last line of `report` with the spaces squeezed down.""" + last_line = report.split('\n')[-2] + return re.sub(r"\s+", " ", last_line) + + def test_report(self): + out = self.run_command("coverage -x mycode.py") + self.assertEqual(out, 'done\n') + report = self.report_from_command("coverage -r") + + # Name Stmts Miss Cover + # --------------------------------------------------------------------- + # c:/ned/coverage/trunk/test/modules/covmod1 2 0 100% + # c:/ned/coverage/trunk/test/zipmods.zip/covmodzip1 2 0 100% + # mycode 4 0 100% + # --------------------------------------------------------------------- + # TOTAL 8 0 100% + + self.assertNotIn("/coverage/__init__/", report) + self.assertIn("/test/modules/covmod1 ", report) + self.assertIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) + self.assertEqual(self.last_line_squeezed(report), "TOTAL 8 0 100%") + + def test_report_just_one(self): + # Try reporting just one module + self.run_command("coverage -x mycode.py") + report = self.report_from_command("coverage -r mycode.py") + + # Name Stmts Miss Cover + # ---------------------------- + # mycode 4 0 100% + + self.assertEqual(self.line_count(report), 3) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/test/modules/covmod1 ", report) + self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) + self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") + + def test_report_omitting(self): + # Try reporting while omitting some modules + prefix = os.path.split(__file__)[0] + self.run_command("coverage -x mycode.py") + report = self.report_from_command("coverage -r -o '%s/*'" % prefix) + + # Name Stmts Miss Cover + # ---------------------------- + # mycode 4 0 100% + + self.assertEqual(self.line_count(report), 3) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/test/modules/covmod1 ", report) + self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) + self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") + + def test_report_including(self): + # Try reporting while including some modules + self.run_command("coverage run mycode.py") + report = self.report_from_command("coverage report --include=mycode*") + + # Name Stmts Miss Cover + # ---------------------------- + # mycode 4 0 100% + + self.assertEqual(self.line_count(report), 3) + self.assertNotIn("/coverage/", report) + self.assertNotIn("/test/modules/covmod1 ", report) + self.assertNotIn("/test/zipmods.zip/covmodzip1 ", report) + self.assertIn("mycode ", report) + self.assertEqual(self.last_line_squeezed(report), "mycode 4 0 100%") + + def test_report_branches(self): + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + out = self.run_command("coverage run --branch mybranch.py") + self.assertEqual(out, 'x\n') + report = self.report_from_command("coverage report") + + # Name Stmts Miss Branch BrMiss Cover + # -------------------------------------------- + # mybranch 5 0 2 1 85% + + self.assertEqual(self.line_count(report), 3) + self.assertIn("mybranch ", report) + self.assertEqual(self.last_line_squeezed(report), + "mybranch 5 0 2 1 86%") + + def test_dotpy_not_python(self): + # We run a .py file, and when reporting, we can't parse it as Python. + # We should get an error message in the report. + + self.run_command("coverage run mycode.py") + self.make_file("mycode.py", "This isn't python at all!") + report = self.report_from_command("coverage -r mycode.py") + + # pylint: disable=C0301 + # Name Stmts Miss Cover + # ---------------------------- + # mycode NotPython: Couldn't parse '/tmp/test_cover/63354509363/mycode.py' as Python source: 'invalid syntax' at line 1 + + last = self.last_line_squeezed(report) + # The actual file name varies run to run. + last = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", last) + # The actual error message varies version to version + last = re.sub(r": '.*' at", ": 'error' at", last) + self.assertEqual(last, + "mycode NotPython: " + "Couldn't parse 'mycode.py' as Python source: " + "'error' at line 1" + ) + + def test_dotpy_not_python_ignored(self): + # We run a .py file, and when reporting, we can't parse it as Python, + # but we've said to ignore errors, so there's no error reported. + self.run_command("coverage run mycode.py") + self.make_file("mycode.py", "This isn't python at all!") + report = self.report_from_command("coverage -r -i mycode.py") + + # Name Stmts Miss Cover + # ---------------------------- + + self.assertEqual(self.line_count(report), 2) + + def test_dothtml_not_python(self): + # We run a .html file, and when reporting, we can't parse it as + # Python. Since it wasn't .py, no error is reported. + + # Run an "html" file + self.make_file("mycode.html", "a = 1") + self.run_command("coverage run mycode.html") + # Before reporting, change it to be an HTML file. + self.make_file("mycode.html", "

    This isn't python at all!

    ") + report = self.report_from_command("coverage -r mycode.html") + + # Name Stmts Miss Cover + # ---------------------------- + + self.assertEqual(self.line_count(report), 2) + + def get_report(self, cov): + """Get the report from `cov`, and canonicalize it.""" + repout = StringIO() + cov.report(file=repout, show_missing=False) + report = repout.getvalue().replace('\\', '/') + report = re.sub(r" +", " ", report) + return report + + def test_bug_156_file_not_run_should_be_zero(self): + # https://bitbucket.org/ned/coveragepy/issue/156 + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + self.make_file("main.py", """\ + print("y") + """) + cov = coverage.coverage(branch=True, source=["."]) + cov.start() + import main # pragma: nested # pylint: disable=F0401,W0612 + cov.stop() # pragma: nested + report = self.get_report(cov).splitlines() + self.assertIn("mybranch 5 5 2 2 0%", report) + + def run_TheCode_and_report_it(self): + """A helper for the next few tests.""" + cov = coverage.coverage() + cov.start() + import TheCode # pragma: nested # pylint: disable=F0401,W0612 + cov.stop() # pragma: nested + return self.get_report(cov) + + def test_bug_203_mixed_case_listed_twice_with_rc(self): + self.make_file("TheCode.py", "a = 1\n") + self.make_file(".coveragerc", "[run]\nsource = .\n") + + report = self.run_TheCode_and_report_it() + + self.assertIn("TheCode", report) + self.assertNotIn("thecode", report) + + def test_bug_203_mixed_case_listed_twice(self): + self.make_file("TheCode.py", "a = 1\n") + + report = self.run_TheCode_and_report_it() + + self.assertIn("TheCode", report) + self.assertNotIn("thecode", report) + + +class SummaryTest2(CoverageTest): + """Another bunch of summary tests.""" + # This class exists because tests naturally clump into classes based on the + # needs of their setUp and tearDown, rather than the product features they + # are testing. There's probably a better way to organize these. + + run_in_temp_dir = False + + def setUp(self): + super(SummaryTest2, self).setUp() + # Parent class saves and restores sys.path, we can just modify it. + this_dir = os.path.dirname(__file__) + sys.path.append(self.nice_file(this_dir, 'modules')) + sys.path.append(self.nice_file(this_dir, 'moremodules')) + + def test_empty_files(self): + # Shows that empty files like __init__.py are listed as having zero + # statements, not one statement. + cov = coverage.coverage() + cov.start() + import usepkgs # pragma: nested # pylint: disable=F0401,W0612 + cov.stop() # pragma: nested + + repout = StringIO() + cov.report(file=repout, show_missing=False) + + report = repout.getvalue().replace('\\', '/') + report = re.sub(r"\s+", " ", report) + self.assertIn("test/modules/pkg1/__init__ 1 0 100%", report) + self.assertIn("test/modules/pkg2/__init__ 0 0 100%", report) + + +class ReportingReturnValue(CoverageTest): + """Tests of reporting functions returning values.""" + + def run_coverage(self): + """Run coverage on doit.py and return the coverage object.""" + self.make_file("doit.py", """\ + a = 1 + b = 2 + c = 3 + d = 4 + if a > 10: + f = 6 + g = 7 + """) + + cov = coverage.coverage() + self.start_import_stop(cov, "doit") + return cov + + def test_report(self): + cov = self.run_coverage() + val = cov.report(include="*/doit.py") + self.assertAlmostEqual(val, 85.7, 1) + + def test_html(self): + cov = self.run_coverage() + val = cov.html_report(include="*/doit.py") + self.assertAlmostEqual(val, 85.7, 1) + + def test_xml(self): + cov = self.run_coverage() + val = cov.xml_report(include="*/doit.py") + self.assertAlmostEqual(val, 85.7, 1) diff --git a/test/test_templite.py b/test/test_templite.py new file mode 100644 index 0000000..0435c54 --- /dev/null +++ b/test/test_templite.py @@ -0,0 +1,204 @@ +"""Tests for coverage.templite.""" + +from coverage.templite import Templite +import unittest + +# pylint: disable=W0612,E1101 +# Disable W0612 (Unused variable) and +# E1101 (Instance of 'foo' has no 'bar' member) + +class AnyOldObject(object): + """Simple testing object. + + Use keyword arguments in the constructor to set attributes on the object. + + """ + def __init__(self, **attrs): + for n, v in attrs.items(): + setattr(self, n, v) + + +class TempliteTest(unittest.TestCase): + """Tests for Templite.""" + + def try_render(self, text, ctx, result): + """Render `text` through `ctx`, and it had better be `result`.""" + self.assertEqual(Templite(text).render(ctx), result) + + def test_passthrough(self): + # Strings without variables are passed through unchanged. + self.assertEqual(Templite("Hello").render(), "Hello") + self.assertEqual( + Templite("Hello, 20% fun time!").render(), + "Hello, 20% fun time!" + ) + + def test_variables(self): + # Variables use {{var}} syntax. + self.try_render("Hello, {{name}}!", {'name':'Ned'}, "Hello, Ned!") + + def test_pipes(self): + # Variables can be filtered with pipes. + data = { + 'name': 'Ned', + 'upper': lambda x: x.upper(), + 'second': lambda x: x[1], + } + self.try_render("Hello, {{name|upper}}!", data, "Hello, NED!") + + # Pipes can be concatenated. + self.try_render("Hello, {{name|upper|second}}!", data, "Hello, E!") + + def test_reusability(self): + # A single Templite can be used more than once with different data. + globs = { + 'upper': lambda x: x.upper(), + 'punct': '!', + } + + template = Templite("This is {{name|upper}}{{punct}}", globs) + self.assertEqual(template.render({'name':'Ned'}), "This is NED!") + self.assertEqual(template.render({'name':'Ben'}), "This is BEN!") + + def test_attribute(self): + # Variables' attributes can be accessed with dots. + obj = AnyOldObject(a="Ay") + self.try_render("{{obj.a}}", locals(), "Ay") + + obj2 = AnyOldObject(obj=obj, b="Bee") + self.try_render("{{obj2.obj.a}} {{obj2.b}}", locals(), "Ay Bee") + + def test_member_function(self): + # Variables' member functions can be used, as long as they are nullary. + class WithMemberFns(AnyOldObject): + """A class to try out member function access.""" + def ditto(self): + """Return twice the .txt attribute.""" + return self.txt + self.txt + obj = WithMemberFns(txt="Once") + self.try_render("{{obj.ditto}}", locals(), "OnceOnce") + + def test_item_access(self): + # Variables' items can be used. + d = {'a':17, 'b':23} + self.try_render("{{d.a}} < {{d.b}}", locals(), "17 < 23") + + def test_loops(self): + # Loops work like in Django. + nums = [1,2,3,4] + self.try_render( + "Look: {% for n in nums %}{{n}}, {% endfor %}done.", + locals(), + "Look: 1, 2, 3, 4, done." + ) + # Loop iterables can be filtered. + def rev(l): + """Return the reverse of `l`.""" + l = l[:] + l.reverse() + return l + + self.try_render( + "Look: {% for n in nums|rev %}{{n}}, {% endfor %}done.", + locals(), + "Look: 4, 3, 2, 1, done." + ) + + def test_empty_loops(self): + self.try_render( + "Empty: {% for n in nums %}{{n}}, {% endfor %}done.", + {'nums':[]}, + "Empty: done." + ) + + def test_multiline_loops(self): + self.try_render( + "Look: \n{% for n in nums %}\n{{n}}, \n{% endfor %}done.", + {'nums':[1,2,3]}, + "Look: \n\n1, \n\n2, \n\n3, \ndone." + ) + + def test_multiple_loops(self): + self.try_render( + "{% for n in nums %}{{n}}{% endfor %} and " + "{% for n in nums %}{{n}}{% endfor %}", + {'nums': [1,2,3]}, + "123 and 123" + ) + + def test_comments(self): + # Single-line comments work: + self.try_render( + "Hello, {# Name goes here: #}{{name}}!", + {'name':'Ned'}, "Hello, Ned!" + ) + # and so do multi-line comments: + self.try_render( + "Hello, {# Name\ngoes\nhere: #}{{name}}!", + {'name':'Ned'}, "Hello, Ned!" + ) + + def test_if(self): + self.try_render( + "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!", + {'ned': 1, 'ben': 0}, + "Hi, NED!" + ) + self.try_render( + "Hi, {% if ned %}NED{% endif %}{% if ben %}BEN{% endif %}!", + {'ned': 0, 'ben': 1}, + "Hi, BEN!" + ) + self.try_render( + "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!", + {'ned': 0, 'ben': 0}, + "Hi, !" + ) + self.try_render( + "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!", + {'ned': 1, 'ben': 0}, + "Hi, NED!" + ) + self.try_render( + "Hi, {% if ned %}NED{% if ben %}BEN{% endif %}{% endif %}!", + {'ned': 1, 'ben': 1}, + "Hi, NEDBEN!" + ) + + def test_loop_if(self): + self.try_render( + "@{% for n in nums %}{% if n %}Z{% endif %}{{n}}{% endfor %}!", + {'nums': [0,1,2]}, + "@0Z1Z2!" + ) + self.try_render( + "X{%if nums%}@{% for n in nums %}{{n}}{% endfor %}{%endif%}!", + {'nums': [0,1,2]}, + "X@012!" + ) + self.try_render( + "X{%if nums%}@{% for n in nums %}{{n}}{% endfor %}{%endif%}!", + {'nums': []}, + "X!" + ) + + def test_nested_loops(self): + self.try_render( + "@{% for n in nums %}" + "{% for a in abc %}{{a}}{{n}}{% endfor %}" + "{% endfor %}!", + {'nums': [0,1,2], 'abc': ['a', 'b', 'c']}, + "@a0b0c0a1b1c1a2b2c2!" + ) + + def test_exception_during_evaluation(self): + # TypeError: Couldn't evaluate {{ foo.bar.baz }}: + # 'NoneType' object is unsubscriptable + self.assertRaises(TypeError, self.try_render, + "Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there" + ) + + def test_bogus_tag_syntax(self): + self.assertRaises(SyntaxError, self.try_render, + "Huh: {% bogus %}!!{% endbogus %}??", {}, "" + ) diff --git a/test/test_testing.py b/test/test_testing.py new file mode 100644 index 0000000..c2d1453 --- /dev/null +++ b/test/test_testing.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +"""Tests that our test infrastructure is really working!""" + +import os, sys +from coverage.backward import to_bytes, rpartition +from test.backunittest import TestCase +from test.coveragetest import CoverageTest + +from coverage.backward import set # pylint: disable=W0622 + +class TestingTest(TestCase): + """Tests of helper methods on `backunittest.TestCase`.""" + + run_in_temp_dir = False + + def please_raise(self, exc, msg): + """Raise an exception for testing assertRaisesRegexp.""" + raise exc(msg) + + def please_succeed(self): + """A simple successful method for testing assertRaisesRegexp.""" + return "All is well" + + def test_assert_same_elements(self): + self.assertSameElements(set(), set()) + self.assertSameElements(set([1,2,3]), set([3,1,2])) + self.assertRaises(AssertionError, self.assertSameElements, + set([1,2,3]), set() + ) + self.assertRaises(AssertionError, self.assertSameElements, + set([1,2,3]), set([4,5,6]) + ) + + def test_assert_regexp_matches(self): + self.assertRegexpMatches("hello", "hel*o") + self.assertRegexpMatches("Oh, hello there!", "hel*o") + self.assertRaises(AssertionError, self.assertRegexpMatches, + "hello there", "^hello$" + ) + + def test_assert_multiline_equal(self): + self.assertMultiLineEqual("hello", "hello") + self.assertRaises(AssertionError, self.assertMultiLineEqual, + "hello there", "Hello there" + ) + self.assertRaises(AssertionError, self.assertMultiLineEqual, + "hello\nthere", "hello\nThere" + ) + # With messages also. + self.assertMultiLineEqual("hi", "hi", "it's ok") + self.assertRaisesRegexp( + AssertionError, "my message", + self.assertMultiLineEqual, "xyz", "abc", "my message" + ) + + def test_assert_raises_regexp(self): + # Raising the right error with the right message passes. + self.assertRaisesRegexp( + ZeroDivisionError, "Wow! Zero!", + self.please_raise, ZeroDivisionError, "Wow! Zero!" + ) + # Raising the right error with a match passes. + self.assertRaisesRegexp( + ZeroDivisionError, "Zero", + self.please_raise, ZeroDivisionError, "Wow! Zero!" + ) + # Raising the right error with a mismatch fails. + self.assertRaises(AssertionError, + self.assertRaisesRegexp, ZeroDivisionError, "XYZ", + self.please_raise, ZeroDivisionError, "Wow! Zero!" + ) + # Raising the right error with a mismatch fails. + self.assertRaises(AssertionError, + self.assertRaisesRegexp, ZeroDivisionError, "XYZ", + self.please_raise, ZeroDivisionError, "Wow! Zero!" + ) + # Raising the wrong error raises the error itself. + self.assertRaises(ZeroDivisionError, + self.assertRaisesRegexp, IOError, "Wow! Zero!", + self.please_raise, ZeroDivisionError, "Wow! Zero!" + ) + # Raising no error fails. + self.assertRaises(AssertionError, + self.assertRaisesRegexp, ZeroDivisionError, "XYZ", + self.please_succeed + ) + + def test_assert_true(self): + self.assertTrue(True) + self.assertRaises(AssertionError, self.assertTrue, False) + + def test_assert_false(self): + self.assertFalse(False) + self.assertRaises(AssertionError, self.assertFalse, True) + + def test_assert_in(self): + self.assertIn("abc", "hello abc") + self.assertIn("abc", ["xyz", "abc", "foo"]) + self.assertIn("abc", {'abc': 1, 'xyz': 2}) + self.assertRaises(AssertionError, self.assertIn, "abc", "xyz") + self.assertRaises(AssertionError, self.assertIn, "abc", ["x", "xabc"]) + self.assertRaises(AssertionError, self.assertIn, "abc", {'x':'abc'}) + + def test_assert_not_in(self): + self.assertRaises(AssertionError, self.assertNotIn, "abc", "hello abc") + self.assertRaises(AssertionError, + self.assertNotIn, "abc", ["xyz", "abc", "foo"] + ) + self.assertRaises(AssertionError, + self.assertNotIn, "abc", {'abc': 1, 'xyz': 2} + ) + self.assertNotIn("abc", "xyz") + self.assertNotIn("abc", ["x", "xabc"]) + self.assertNotIn("abc", {'x':'abc'}) + + def test_assert_greater(self): + self.assertGreater(10, 9) + self.assertGreater("xyz", "abc") + self.assertRaises(AssertionError, self.assertGreater, 9, 10) + self.assertRaises(AssertionError, self.assertGreater, 10, 10) + self.assertRaises(AssertionError, self.assertGreater, "abc", "xyz") + self.assertRaises(AssertionError, self.assertGreater, "xyz", "xyz") + + +class CoverageTestTest(CoverageTest): + """Test the methods in `CoverageTest`.""" + + def file_text(self, fname): + """Return the text read from a file.""" + return open(fname, "rb").read().decode('ascii') + + def test_make_file(self): + # A simple file. + self.make_file("fooey.boo", "Hello there") + self.assertEqual(open("fooey.boo").read(), "Hello there") + # A file in a sub-directory + self.make_file("sub/another.txt", "Another") + self.assertEqual(open("sub/another.txt").read(), "Another") + # A second file in that sub-directory + self.make_file("sub/second.txt", "Second") + self.assertEqual(open("sub/second.txt").read(), "Second") + # A deeper directory + self.make_file("sub/deeper/evenmore/third.txt") + self.assertEqual(open("sub/deeper/evenmore/third.txt").read(), "") + + def test_make_file_newline(self): + self.make_file("unix.txt", "Hello\n") + self.assertEqual(self.file_text("unix.txt"), "Hello\n") + self.make_file("dos.txt", "Hello\n", newline="\r\n") + self.assertEqual(self.file_text("dos.txt"), "Hello\r\n") + self.make_file("mac.txt", "Hello\n", newline="\r") + self.assertEqual(self.file_text("mac.txt"), "Hello\r") + + def test_make_file_non_ascii(self): + self.make_file("unicode.txt", "tabblo: «ταБЬℓσ»") + self.assertEqual( + open("unicode.txt", "rb").read(), + to_bytes("tabblo: «ταБЬℓσ»") + ) + + def test_file_exists(self): + self.make_file("whoville.txt", "We are here!") + self.assert_exists("whoville.txt") + self.assert_doesnt_exist("shadow.txt") + self.assertRaises( + AssertionError, self.assert_doesnt_exist, "whoville.txt" + ) + self.assertRaises(AssertionError, self.assert_exists, "shadow.txt") + + def test_sub_python_is_this_python(self): + # Try it with a python command. + os.environ['COV_FOOBAR'] = 'XYZZY' + self.make_file("showme.py", """\ + import os, sys + print(sys.executable) + print(os.__file__) + print(os.environ['COV_FOOBAR']) + """) + out = self.run_command("python showme.py").splitlines() + self.assertEqual(out[0], sys.executable) + self.assertEqual(out[1], os.__file__) + self.assertEqual(out[2], 'XYZZY') + + # Try it with a "coverage debug sys" command. + out = self.run_command("coverage debug sys").splitlines() + # "environment: COV_FOOBAR = XYZZY" or "COV_FOOBAR = XYZZY" + executable = [l for l in out if "executable:" in l][0] + executable = executable.split(":", 1)[1].strip() + self.assertEqual(executable, sys.executable) + environ = [l for l in out if "COV_FOOBAR" in l][0] + _, _, environ = rpartition(environ, ":") + self.assertEqual(environ.strip(), "COV_FOOBAR = XYZZY") diff --git a/test/test_xml.py b/test/test_xml.py new file mode 100644 index 0000000..204b586 --- /dev/null +++ b/test/test_xml.py @@ -0,0 +1,84 @@ +"""Tests for XML reports from coverage.py.""" + +import os, re +import coverage + +from test.coveragetest import CoverageTest + +class XmlReportTest(CoverageTest): + """Tests of the XML reports from coverage.py.""" + + def run_mycode(self): + """Run mycode.py, so we can report on it.""" + self.make_file("mycode.py", "print('hello')\n") + self.run_command("coverage run mycode.py") + + def test_default_file_placement(self): + self.run_mycode() + self.run_command("coverage xml") + self.assert_exists("coverage.xml") + + def test_argument_affects_xml_placement(self): + self.run_mycode() + self.run_command("coverage xml -o put_it_there.xml") + self.assert_doesnt_exist("coverage.xml") + self.assert_exists("put_it_there.xml") + + def test_config_affects_xml_placement(self): + self.run_mycode() + self.make_file(".coveragerc", "[xml]\noutput = xml.out\n") + self.run_command("coverage xml") + self.assert_doesnt_exist("coverage.xml") + self.assert_exists("xml.out") + + def test_no_data(self): + # https://bitbucket.org/ned/coveragepy/issue/210 + self.run_command("coverage xml") + self.assert_doesnt_exist("coverage.xml") + + def test_no_source(self): + # Written while investigating a bug, might as well keep it. + # https://bitbucket.org/ned/coveragepy/issue/208 + self.make_file("innocuous.py", "a = 4") + cov = coverage.coverage() + self.start_import_stop(cov, "innocuous") + os.remove("innocuous.py") + cov.xml_report(ignore_errors=True) + self.assert_exists("coverage.xml") + + def run_doit(self): + """Construct a simple sub-package.""" + self.make_file("sub/__init__.py") + self.make_file("sub/doit.py", "print('doit!')") + self.make_file("main.py", "import sub.doit") + cov = coverage.coverage() + self.start_import_stop(cov, "main") + return cov + + def test_filename_format_showing_everything(self): + cov = self.run_doit() + cov.xml_report(outfile="-") + xml = self.stdout() + doit_line = re_line(xml, "class.*doit") + self.assertIn('filename="sub/doit.py"', doit_line) + + def test_filename_format_including_filename(self): + cov = self.run_doit() + cov.xml_report(["sub/doit.py"], outfile="-") + xml = self.stdout() + doit_line = re_line(xml, "class.*doit") + self.assertIn('filename="sub/doit.py"', doit_line) + + def test_filename_format_including_module(self): + cov = self.run_doit() + import sub.doit # pylint: disable=F0401 + cov.xml_report([sub.doit], outfile="-") + xml = self.stdout() + doit_line = re_line(xml, "class.*doit") + self.assertIn('filename="sub/doit.py"', doit_line) + + +def re_line(text, pat): + """Return the one line in `text` that matches regex `pat`.""" + lines = [l for l in text.splitlines() if re.search(pat, l)] + return lines[0] diff --git a/test/try_execfile.py b/test/try_execfile.py new file mode 100644 index 0000000..9bbabd1 --- /dev/null +++ b/test/try_execfile.py @@ -0,0 +1,34 @@ +"""Test file for run_python_file.""" + +import os, pprint, sys + +DATA = "xyzzy" + +import __main__ + +def my_function(a): + """A function to force execution of module-level values.""" + return "my_fn(%r)" % a + +FN_VAL = my_function("fooey") + +try: + pkg = __package__ +except NameError: + pkg = "*No __package__*" + +globals_to_check = { + '__name__': __name__, + '__file__': __file__, + '__doc__': __doc__, + '__builtins__.has_open': hasattr(__builtins__, 'open'), + '__builtins__.dir': dir(__builtins__), + '__package__': pkg, + 'DATA': DATA, + 'FN_VAL': FN_VAL, + '__main__.DATA': getattr(__main__, "DATA", "nothing"), + 'argv': sys.argv, + 'path': [os.path.normcase(p) for p in sys.path], +} + +pprint.pprint(globals_to_check) diff --git a/test_old.sh b/test_old.sh new file mode 100644 index 0000000..72c3b35 --- /dev/null +++ b/test_old.sh @@ -0,0 +1,8 @@ +# Steps to prepare and run coverage.py tests, for Pythons < 2.5 +# This should do the same steps as tox.ini +easy_install nose==1.2.1 mock==0.6.0 +python setup.py --quiet clean develop +python igor.py zip_mods install_egg remove_extension +python igor.py test_with_tracer py +python setup.py --quiet build_ext --inplace +python igor.py test_with_tracer c diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..a09ddbb --- /dev/null +++ b/tox.ini @@ -0,0 +1,32 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py25, py26, py27, py31, py32, py33, pypy + +[testenv] +commands = + {envpython} setup.py --quiet clean develop + + # Create test/zipmods.zip + # Install the egg1 egg + # Remove the C extension so that we can test the PyTracer + {envpython} igor.py zip_mods install_egg remove_extension + + # Test with the PyTracer + {envpython} igor.py test_with_tracer py {posargs} + + # Build the C extension and test with the CTracer + {envpython} setup.py --quiet build_ext --inplace + {envpython} igor.py test_with_tracer c {posargs} + +deps = + nose + mock + +[testenv:pypy] +# PyPy has no C extensions +setenv = + COVERAGE_NO_EXTENSION=1 -- 2.7.4