From 76ec1f6f0f9a2f3c34b59892aadd532e9abcf18e Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 27 Jun 2009 17:17:28 -0400 Subject: PYTHONPATH might not exist (for example on OS/X) --- test/coveragetest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/coveragetest.py b/test/coveragetest.py index 0fb8d16..dad9697 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 -- cgit v1.2.1 From c42de28315ba31847441fcb90393545c87bcd641 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 27 Jun 2009 17:36:13 -0400 Subject: When executing files, open them in Universal Newline mode, just as Python itself does. Makes it possible to run Python from Windows on Mac, for example. --- coverage/execfile.py | 2 +- test/test_execfile.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/coverage/execfile.py b/coverage/execfile.py index d6bcdce..43bdc5c 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/test/test_execfile.py b/test/test_execfile.py index eae227a..6cc7cab 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) + -- cgit v1.2.1 From 356f8d7701685af9a88a9ce4c605d61af1eb5c77 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 27 Jun 2009 17:37:17 -0400 Subject: On Mac/Linux, compiled extensions are .so files. Ignore them. --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index f53ccea..3716b4f 100644 --- a/.hgignore +++ b/.hgignore @@ -4,6 +4,7 @@ syntax: glob *.pyc *.pyo *.pyd +*.so *.bak .coverage .coverage.* -- cgit v1.2.1 From 3d07af1b6936df6d64999076709a51911a6ddc7f Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sat, 27 Jun 2009 17:39:19 -0400 Subject: Ch-ch-ch-ch-changes --- CHANGES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index ca3658f..488ab3d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -8,6 +8,10 @@ Version 3.next - Removed the recursion limit in the tracer function. Previously, code that ran more than 500 frames deep would crash. +- 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 ------------------------- -- cgit v1.2.1 From df28a7b0d859400c1b8216f346e30b6a34d069a9 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 28 Jun 2009 13:39:35 -0400 Subject: Add a bunch of logging to tracer.c to help find tracing problems. --- coverage/tracer.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/coverage/tracer.c b/coverage/tracer.c index b1b82fe..50a25be 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]; -- cgit v1.2.1 From 76b030ce538df81d137ca04a1bf42606c8872749 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 28 Jun 2009 17:43:05 -0400 Subject: Epic bug: pyexpat fiddles incorrectly with the systrace function. This is a hack to make it behave correctly with coverage.py. Fixes bug #10. --- CHANGES.txt | 3 +++ coverage/tracer.c | 19 +++++++++++++++++++ test/test_coverage.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 488ab3d..9f4ec39 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -12,6 +12,9 @@ Version 3.next 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/tracer.c b/coverage/tracer.c index 50a25be..15a17b2 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -198,6 +198,25 @@ 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. 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/test_coverage.py b/test/test_coverage.py index 30ac4b8..2d08281 100644 --- a/test/test_coverage.py +++ b/test/test_coverage.py @@ -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 = '''\\ + + + ''' + + 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. -- cgit v1.2.1 From 78fe06779d163b3521ab85c68924dae1b916b6aa Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 28 Jun 2009 18:46:20 -0400 Subject: Expat bug reported and noted. --- coverage/tracer.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/coverage/tracer.c b/coverage/tracer.c index 15a17b2..ba84204 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -203,9 +203,12 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) 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. 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. + 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")) { -- cgit v1.2.1 From fa6e6b4ead3a0f50cd4c3642df459827692339da Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 28 Jun 2009 22:21:05 -0400 Subject: I forget why this test was neutered. --- test/test_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_coverage.py b/test/test_coverage.py index 2d08281..3575f37 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, """\ -- cgit v1.2.1