summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore1
-rw-r--r--CHANGES.txt7
-rw-r--r--coverage/execfile.py2
-rw-r--r--coverage/tracer.c97
-rw-r--r--test/coveragetest.py2
-rw-r--r--test/test_coverage.py49
-rw-r--r--test/test_execfile.py11
7 files changed, 156 insertions, 13 deletions
diff --git a/.hgignore b/.hgignore
index f53ccead..3716b4f1 100644
--- a/.hgignore
+++ b/.hgignore
@@ -4,6 +4,7 @@ syntax: glob
*.pyc
*.pyo
*.pyd
+*.so
*.bak
.coverage
.coverage.*
diff --git a/CHANGES.txt b/CHANGES.txt
index c0431600..dbb9f4e8 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -12,6 +12,13 @@ Version 3.next
- 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.
+
+- Fixed a bizarre problem involving pyexpat, whereby lines following XML parser
+ invocations could be overlooked.
+
Version 3.0, 13 June 2009
-------------------------
diff --git a/coverage/execfile.py b/coverage/execfile.py
index d6bcdceb..43bdc5c3 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -24,7 +24,7 @@ def run_python_file(filename, args):
sys.path[0] = os.path.dirname(filename)
try:
- source = open(filename).read()
+ source = open(filename, 'rU').read()
exec compile(source, filename, "exec") in main_mod.__dict__
finally:
# Restore the old __main__
diff --git a/coverage/tracer.c b/coverage/tracer.c
index b1b82fe4..ba84204f 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -6,13 +6,8 @@
#include "structmember.h"
#include "frameobject.h"
-#define DEBUG 1
-
-#if DEBUG
-#define IFDEBUG(x) x
-#else
-#define IFDEBUG(x)
-#endif
+#undef WHAT_LOG /* Define to log the WHAT params in the trace function. */
+#undef TRACE_LOG /* Define to log our bookkeeping. */
/* The Tracer type. */
@@ -68,14 +63,72 @@ Tracer_dealloc(Tracer *self)
self->ob_type->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) {
+ printf(" %s", PyString_AS_STRING(filename));
+ }
+ 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
+
static int
Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
{
PyObject * filename = NULL;
PyObject * tracename = NULL;
- /* printf("trace: %d @ %d\n", what, frame->f_lineno); */
-
+ #if WHAT_LOG
+ if (what <= sizeof(what_sym)/sizeof(const char *)) {
+ printf("trace: %s @ %s %d\n", what_sym[what], PyString_AS_STRING(frame->f_code->co_filename), frame->f_lineno);
+ }
+ #endif
+
+ #if TRACE_LOG
+ if (strstr(PyString_AS_STRING(frame->f_code->co_filename), start_file) && frame->f_lineno == start_line) {
+ logging = 1;
+ }
+ #endif
+
switch (what) {
case PyTrace_CALL: /* 0 */
self->depth++;
@@ -112,15 +165,18 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
/* If tracename is a string, then we're supposed to trace. */
if (PyString_Check(tracename)) {
self->tracenames[self->depth] = tracename;
+ SHOWLOG(self->depth, frame->f_lineno, filename, "traced");
}
else {
self->tracenames[self->depth] = NULL;
Py_DECREF(tracename);
+ SHOWLOG(self->depth, frame->f_lineno, filename, "skipped");
}
break;
case PyTrace_RETURN: /* 3 */
if (self->depth >= 0) {
+ SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "return");
Py_XDECREF(self->tracenames[self->depth]);
self->depth--;
}
@@ -128,6 +184,7 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
case PyTrace_LINE: /* 2 */
if (self->depth >= 0) {
+ SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "line");
if (self->tracenames[self->depth]) {
PyObject * t = PyTuple_New(2);
tracename = self->tracenames[self->depth];
@@ -141,6 +198,28 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
break;
}
+ /* UGLY HACK: for some reason, pyexpat invokes the systrace function directly.
+ It uses "pyexpat.c" as the filename, which is strange enough, but it calls
+ it incorrectly: when an exception passes through the C code, it calls trace
+ with an EXCEPTION, but never calls RETURN. This throws off our bookkeeping.
+ To make things right, if this is an EXCEPTION from pyexpat.c, then inject
+ a RETURN event also.
+
+ I've reported the problem with pyexpat.c as http://bugs.python.org/issue6359 .
+ If the bug in pyexpat.c gets fixed someday, we'll either have to put a
+ version check here, or do something more sophisticated to detect the
+ EXCEPTION-without-RETURN case that has to be fixed up.
+ */
+ if (what == PyTrace_EXCEPTION) {
+ if (strstr(PyString_AS_STRING(frame->f_code->co_filename), "pyexpat.c")) {
+ /* Stupid pyexpat: pretend it gave us the RETURN it should have. */
+ SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "wrongexc");
+ if (Tracer_trace(self, frame, PyTrace_RETURN, arg) < 0) {
+ return -1;
+ }
+ }
+ }
+
return 0;
}
diff --git a/test/coveragetest.py b/test/coveragetest.py
index 0fb8d163..dad96978 100644
--- a/test/coveragetest.py
+++ b/test/coveragetest.py
@@ -209,7 +209,7 @@ class CoverageTest(unittest.TestCase):
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.environ['PYTHONPATH']
+ pypath = os.environ.get('PYTHONPATH', '')
if pypath:
pypath += os.pathsep
pypath += testmods + os.pathsep + zipfile
diff --git a/test/test_coverage.py b/test/test_coverage.py
index 30ac4b8c..3575f378 100644
--- a/test/test_coverage.py
+++ b/test/test_coverage.py
@@ -1689,7 +1689,7 @@ class RecursionTest(CoverageTest):
""",
[1,2,3,5,7], "")
- def xxtestLongRecursion(self):
+ def testLongRecursion(self):
# We can't finish a very deep recursion, but we don't crash.
self.assertRaises(RuntimeError, self.checkCoverage,
"""\
@@ -1704,6 +1704,51 @@ class RecursionTest(CoverageTest):
[1,2,3,5,7], "")
+class PyexpatTest(CoverageTest):
+ """Pyexpat screws up tracing. Make sure we've counter-defended properly."""
+ def testPyexpat(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.makeFile("trydom.py", """\
+ import xml.dom.minidom
+
+ XML = '''\\
+ <!DOCTYPE fooey SYSTEM "http://www.example.com/example.dtd">
+ <root><child/><child/></root>
+ '''
+
+ def foo():
+ dom = xml.dom.minidom.parseString(XML)
+ assert len(dom.getElementsByTagName('child')) == 2
+ print "Parsed"
+
+ foo()
+ """)
+
+ self.makeFile("outer.py", "\n"*100 + "import trydom\nprint 'done'\n")
+
+ cov = coverage.coverage()
+ cov.erase()
+
+ # Import the python file, executing it.
+ cov.start()
+ self.importModule("outer")
+ cov.stop()
+
+ _, 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, [])
+
+
if __name__ == '__main__':
print "Testing under Python version: %s" % sys.version
unittest.main()
@@ -1714,4 +1759,4 @@ if __name__ == '__main__':
# in an expression!)
# TODO: Generator comprehensions?
# TODO: Constant if tests ("if 1:"). Py 2.4 doesn't execute them.
-# TODO: There are no tests for analysis2 directly. \ No newline at end of file
+# TODO: There are no tests for analysis2 directly.
diff --git a/test/test_execfile.py b/test/test_execfile.py
index eae227a1..6cc7cab8 100644
--- a/test/test_execfile.py
+++ b/test/test_execfile.py
@@ -46,3 +46,14 @@ class RunTest(CoverageTest):
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')
+ fpy.write(nl.join(pylines))
+ fpy.close()
+ run_python_file('nl.py', ['nl.py'])
+ self.assertEqual(self.stdout(), "Hello, world!\n"*3)
+