diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 99c242dd98e..cbadf667907 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -168,6 +168,8 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None values.append(val[0].lineno - 1 - 1) values.sort() insert_index = bisect_right(values, lineno) + if insert_index == 0: + return 0, None start = values[insert_index - 1] if insert_index >= len(values): end = None @@ -216,6 +218,7 @@ def getstatementrange_ast( pass # The end might still point to a comment or empty line, correct it. + end = min(end, len(source.lines)) while end: line = source.lines[end - 1].lstrip() if line.startswith("#") or not line: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index e413af3766e..1afcd906b61 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -7,6 +7,7 @@ import sys import textwrap from typing import Any +from unittest.mock import patch from _pytest._code import Code from _pytest._code import Frame @@ -647,3 +648,26 @@ def __init__(self, *args): # fmt: on values = [i for i in x.source.lines if i.strip()] assert len(values) == 4 + + +def test_patched_compile() -> None: + # ensure Source doesn't break + # when compile() modifies code dynamically + from builtins import compile + + def patched_compile1(_, *args, **kwargs): + return compile("", *args, **kwargs) + + with patch("builtins.compile", new=patched_compile1): + Source(patched_compile1).getstatement(1) + + # fmt: off + def patched_compile2(_, *args, **kwargs): + # first line of this function must not start with spaces + # LINES must be equal to number of lines of this function + LINES = 4 + return compile("\ndef a():\n" + "\n" * LINES + " pass", *args, **kwargs) + # fmt: on + + with patch("builtins.compile", new=patched_compile2): + Source(patched_compile2).getstatement(1)