Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions gunicorn/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ def __call__(self, frame, event, arg):
else:
name = '[unknown]'
try:
src = inspect.getsourcelines(frame)
line = src[lineno]
except OSError:
src_lines, src_start = inspect.getsourcelines(frame)
line = src_lines[lineno - src_start]
except (OSError, IndexError):
line = 'Unknown code named [%s]. VM instruction #%d' % (
frame.f_code.co_name, frame.f_lasti)
if self.trace_names is None or name in self.trace_names:
Expand Down
61 changes: 61 additions & 0 deletions tests/test_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.

"""Tests for the debug module (--spew functionality)."""

import io
import linecache
import sys

from gunicorn.debug import Spew


class TestSpew:
"""Tests for the Spew trace hook used by --spew."""

def test_getsourcelines_indexing(self):
"""Verify that Spew correctly indexes inspect.getsourcelines() result.

Regression test for https://github.com/benoitc/gunicorn/issues/3344

inspect.getsourcelines() returns (lines_list, start_lineno), a tuple
of length 2. The old code did ``src[lineno]`` which indexed the tuple
by the frame's line number instead of indexing the list of source
lines. When the line number was 1, ``src[1]`` returned the integer
start_lineno, causing ``AttributeError: 'int' object has no attribute
'rstrip'``. For any line number >= 2, it raised an IndexError.
"""
spew = Spew(show_values=False)

# Register source in linecache for a dynamically compiled function
# so that inspect.getsourcelines() succeeds even though __file__ is
# absent from f_globals -- this is exactly the pattern that triggers
# the bug (e.g. code dynamically generated by the attrs library).
filename = "<spew-test-generated>"
source = "def _test_fn():\n x = 1\n return x\n"
lines = source.splitlines(True)
linecache.cache[filename] = (len(source), None, lines, filename)

code = compile(source, filename, "exec")
# Execute without __file__ in globals to trigger the fallback path
ns = {"__name__": "__spew_test__"}
exec(code, ns)
fn = ns["_test_fn"]

old_stdout = sys.stdout
sys.stdout = captured = io.StringIO()
try:
sys.settrace(spew)
fn()
finally:
sys.settrace(None)
sys.stdout = old_stdout

output = captured.getvalue()
# The trace hook should have printed source lines without crashing.
# Before the fix, this would crash with:
# AttributeError: 'int' object has no attribute 'rstrip'
assert output, "Expected trace output but got nothing"
assert "x = 1" in output or "return x" in output