diff options
author | Ned Batchelder <ned@nedbatchelder.com> | 2022-01-15 13:58:53 -0500 |
---|---|---|
committer | Ned Batchelder <ned@nedbatchelder.com> | 2022-01-15 14:18:00 -0500 |
commit | 37ef7c7d8625ee7f364774110e3c467e82444d9b (patch) | |
tree | 4bd314b3bee2c8502437ef59edc51778433d8d4e | |
parent | 7fec9566c74d46b95f6a741a59e66e136cc5b158 (diff) | |
download | python-coveragepy-git-37ef7c7d8625ee7f364774110e3c467e82444d9b.tar.gz |
fix: proper tracing of call/return for Python 3.11.0a4
Version 3.11.0a4 introduced RESUME, so returns and calls are different now.
This change also fixes some mishandling of yield-from in previous releases.
-rw-r--r-- | CHANGES.rst | 4 | ||||
-rw-r--r-- | coverage/ctracer/tracer.c | 49 | ||||
-rw-r--r-- | coverage/pytracer.py | 23 | ||||
-rw-r--r-- | tests/test_arcs.py | 2 |
4 files changed, 59 insertions, 19 deletions
diff --git a/CHANGES.rst b/CHANGES.rst index 670f3363..8bd70d68 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -22,7 +22,9 @@ This list is detailed and covers changes in each pre-release version. Unreleased ---------- -- Dropped support for Python 3.6, which ended support on 2021-12-23. +- Dropped support for Python 3.6, which reached end-of-life on 2021-12-23. + +- Updated Python 3.11 support to 3.11.0a4. - Fix: a .gitignore file will only be written into the HTML report output directory if the directory is empty. This should prevent certain unfortunate diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 4227e7da..fc88ef85 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -520,10 +520,24 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) Py_XSETREF(frame->f_trace, (PyObject*)self); /* A call event is really a "start frame" event, and can happen for - * re-entering a generator also. f_lasti is -1 for a true call, and a - * real byte offset for a generator re-entry. + * re-entering a generator also. How we tell the difference depends on + * the version of Python. */ - if (MyFrame_lasti(frame) < 0) { + BOOL real_call = FALSE; + +#ifdef RESUME // 3.11.0a4 + /* + * The current opcode is guaranteed to be RESUME. The argument + * determines what kind of resume it is. + */ + PyObject * pCode = MyFrame_GetCode(frame)->co_code; + real_call = (PyBytes_AS_STRING(pCode)[MyFrame_lasti(frame) + 1] == 0); +#else + // f_lasti is -1 for a true call, and a real byte offset for a generator re-entry. + real_call = (MyFrame_lasti(frame) < 0); +#endif + + if (real_call) { self->pcur_entry->last_line = -MyFrame_GetCode(frame)->co_firstlineno; } else { @@ -683,19 +697,36 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) if (self->pdata_stack->depth >= 0) { if (self->tracing_arcs && self->pcur_entry->file_data) { + BOOL real_return = FALSE; + PyObject * pCode = MyFrame_GetCode(frame)->co_code; + int lasti = MyFrame_lasti(frame); + Py_ssize_t code_size = PyBytes_GET_SIZE(pCode); + unsigned char * code_bytes = (unsigned char *)PyBytes_AS_STRING(pCode); +#ifdef RESUME + if (lasti == code_size - 2) { + real_return = TRUE; + } + else { + real_return = (code_bytes[lasti + 2] != RESUME); + } +#else /* Need to distinguish between RETURN_VALUE and YIELD_VALUE. Read * the current bytecode to see what it is. In unusual circumstances * (Cython code), co_code can be the empty string, so range-check * f_lasti before reading the byte. */ - int bytecode = RETURN_VALUE; - PyObject * pCode = MyFrame_GetCode(frame)->co_code; - int lasti = MyFrame_lasti(frame); + BOOL is_yield = FALSE; + BOOL is_yield_from = FALSE; - if (lasti < PyBytes_GET_SIZE(pCode)) { - bytecode = PyBytes_AS_STRING(pCode)[lasti]; + if (lasti < code_size) { + is_yield = (code_bytes[lasti] == YIELD_VALUE); + if (lasti + 2 < code_size) { + is_yield_from = (code_bytes[lasti + 2] == YIELD_FROM); + } } - if (bytecode != YIELD_VALUE) { + real_return = !(is_yield || is_yield_from); +#endif + if (real_return) { int first = MyFrame_GetCode(frame)->co_firstlineno; if (CTracer_record_pair(self, self->pcur_entry->last_line, -first) < 0) { goto error; diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 94712b24..7709df34 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -168,10 +168,10 @@ class PyTracer: # The current opcode is guaranteed to be RESUME. The argument # determines what kind of resume it is. oparg = frame.f_code.co_code[frame.f_lasti + 1] - true_call = (oparg == 0) + real_call = (oparg == 0) else: - true_call = (getattr(frame, 'f_lasti', -1) < 0) - if true_call: + real_call = (getattr(frame, 'f_lasti', -1) < 0) + if real_call: self.last_line = -frame.f_code.co_firstlineno else: self.last_line = frame.f_lineno @@ -194,13 +194,22 @@ class PyTracer: if RESUME is not None: if len(code) == lasti + 2: # A return from the end of a code object is a real return. - true_return = True + real_return = True else: # it's a real return. - true_return = (code[lasti + 2] != RESUME) + real_return = (code[lasti + 2] != RESUME) else: - true_return = not ( (code[lasti] == YIELD_VALUE) or ((len(code) > lasti + YIELD_FROM_OFFSET) and code[lasti + YIELD_FROM_OFFSET] == YIELD_FROM) ) - if true_return: + if code[lasti] == RETURN_VALUE: + real_return = True + elif code[lasti] == YIELD_VALUE: + real_return = False + elif len(code) <= lasti + YIELD_FROM_OFFSET: + real_return = True + elif code[lasti + YIELD_FROM_OFFSET] == YIELD_FROM: + real_return = False + else: + real_return = True + if real_return: first = frame.f_code.co_firstlineno self.cur_file_data.add((self.last_line, -first)) # Leaving this function, pop the filename stack. diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 35c1ed3e..39187598 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1288,7 +1288,6 @@ class YieldTest(CoverageTest): list(gen([1,2,3])) """, arcz=".1 19 9. .2 23 34 45 56 63 37 7.", - arcz_unpredicted="5.", ) def test_abandoned_yield(self): @@ -1866,7 +1865,6 @@ class AsyncTest(CoverageTest): ".1 13 38 8E EF FG G. " + "-34 45 56 6-3 " + "-89 9C C-8", - arcz_unpredicted="5-3 9-8", ) assert self.stdout() == "Compute 1 + 2 ...\n1 + 2 = 3\n" |