Skip to content
This repository was archived by the owner on Jan 23, 2024. It is now read-only.

Commit 3d3e066

Browse files
authored
fix: handle negative line number deltas (#73)
In Python 3.6-3.9, unlike in Python 2, co_lnotab's line number offsets can be negative and are represented by a signed integer. The Debugger agent had been using an unsigned integer which prevented it from being able to set breakpoints on lines of code in a code object after the line table had a negative offset. This PR also enables most of the previously disabled tests now that they work on all supported versions of Python. A couple still remain disabled and will be fixed later on.
1 parent bb3fe49 commit 3d3e066

File tree

5 files changed

+59
-50
lines changed

5 files changed

+59
-50
lines changed

src/googleclouddebugger/module_explorer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def GetCodeObjectAtLine(module, line):
6262
prev_line = max(prev_line, co_line_number)
6363
elif co_line_number > line:
6464
next_line = min(next_line, co_line_number)
65-
break
65+
# Continue because line numbers may not be sequential.
6666

6767
prev_line = None if prev_line == 0 else prev_line
6868
next_line = None if next_line == sys.maxsize else next_line
@@ -87,6 +87,9 @@ def _GetLineNumbers(code_object):
8787
line_incrs = code_object.co_lnotab[1::2]
8888
current_line = code_object.co_firstlineno
8989
for line_incr in line_incrs:
90+
if line_incr >= 0x80:
91+
# line_incrs is an array of 8-bit signed integers
92+
line_incr -= 0x100
9093
current_line += line_incr
9194
yield current_line
9295
else:

src/googleclouddebugger/python_util.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ bool CodeObjectLinesEnumerator::Next() {
7979

8080
while (true) {
8181
offset_ += next_entry_[0];
82-
line_number_ += next_entry_[1];
82+
line_number_ += static_cast<int8_t>(next_entry_[1]);
8383

8484
bool stop = ((next_entry_[0] != 0xFF) || (next_entry_[1] != 0)) &&
8585
((next_entry_[0] != 0) || (next_entry_[1] != 0xFF));
Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Complete tests of the debugger mocking the backend."""
22

3-
# TODO: Get this test to work well all supported versions of python.
4-
53
from datetime import datetime
64
from datetime import timedelta
75
import functools
@@ -20,7 +18,7 @@
2018
import google.auth
2119
from absl.testing import absltest
2220

23-
from googleclouddebugger import capture_collector
21+
from googleclouddebugger import collector
2422
from googleclouddebugger import labels
2523
import python_test_util
2624

@@ -98,7 +96,7 @@ def GetVal():
9896
cdbg.enable()
9997

10098
# Increase the polling rate to speed up the test.
101-
cdbg._hub_client.min_interval_sec = 0.001 # Poll every 1 ms
99+
cdbg.gcp_hub_client.min_interval_sec = 0.001 # Poll every 1 ms
102100

103101
def SetBreakpoint(self, tag, template=None):
104102
"""Sets a new breakpoint in this source file.
@@ -214,7 +212,6 @@ def execute(self): # pylint: disable=invalid-name
214212

215213
return FakeBreakpointUpdateCommand(self._incoming_breakpoint_updates)
216214

217-
218215
# We only need to attach the debugger exactly once. The IntegrationTest class
219216
# is created for each test case, so we need to keep this state global.
220217

@@ -226,7 +223,7 @@ def _FakeLog(self, message, extra=None):
226223

227224
def setUp(self):
228225
self._info_log = []
229-
capture_collector.log_info_message = self._FakeLog
226+
collector.log_info_message = self._FakeLog
230227

231228
def tearDown(self):
232229
IntegrationTest._hub.SetActiveBreakpoints([])
@@ -281,8 +278,8 @@ def testExistingLabelsPriority(self):
281278
def Trigger():
282279
print('Breakpoint trigger with labels') # BPTAG: EXISTING_LABELS_PRIORITY
283280

284-
current_labels_collector = capture_collector.breakpoint_labels_collector
285-
capture_collector.breakpoint_labels_collector = \
281+
current_labels_collector = collector.breakpoint_labels_collector
282+
collector.breakpoint_labels_collector = \
286283
lambda: {'label_1': 'value_1', 'label_2': 'value_2'}
287284

288285
IntegrationTest._hub.SetBreakpoint(
@@ -294,7 +291,7 @@ def Trigger():
294291

295292
Trigger()
296293

297-
capture_collector.breakpoint_labels_collector = current_labels_collector
294+
collector.breakpoint_labels_collector = current_labels_collector
298295

299296
# In this case, label_1 was in both the agent and the pre existing labels,
300297
# the pre existing value of value_foobar should be preserved.
@@ -313,14 +310,14 @@ def Trigger():
313310
print('Breakpoint trigger req id label') # BPTAG: REQUEST_LOG_ID_LABEL
314311

315312
current_request_log_id_collector = \
316-
capture_collector.request_log_id_collector
317-
capture_collector.request_log_id_collector = lambda: 'foo_bar_id'
313+
collector.request_log_id_collector
314+
collector.request_log_id_collector = lambda: 'foo_bar_id'
318315

319316
IntegrationTest._hub.SetBreakpoint('REQUEST_LOG_ID_LABEL')
320317

321318
Trigger()
322319

323-
capture_collector.request_log_id_collector = \
320+
collector.request_log_id_collector = \
324321
current_request_log_id_collector
325322

326323
result = IntegrationTest._hub.GetNextResult()
@@ -559,23 +556,25 @@ def Method(a):
559556
}, python_test_util.PackFrameVariable(result, 'x', frame=1))
560557
return x
561558

562-
def testRecursion(self):
563-
564-
def RecursiveMethod(i):
565-
if i == 0:
566-
return 0 # BPTAG: RECURSION
567-
return RecursiveMethod(i - 1)
568-
569-
IntegrationTest._hub.SetBreakpoint('RECURSION')
570-
RecursiveMethod(5)
571-
result = IntegrationTest._hub.GetNextResult()
572559

573-
for frame in range(5):
574-
self.assertEqual({
575-
'name': 'i',
576-
'value': str(frame),
577-
'type': 'int'
578-
}, python_test_util.PackFrameVariable(result, 'i', frame, 'arguments'))
560+
# FIXME: Broken in Python 3.10
561+
# def testRecursion(self):
562+
#
563+
# def RecursiveMethod(i):
564+
# if i == 0:
565+
# return 0 # BPTAG: RECURSION
566+
# return RecursiveMethod(i - 1)
567+
#
568+
# IntegrationTest._hub.SetBreakpoint('RECURSION')
569+
# RecursiveMethod(5)
570+
# result = IntegrationTest._hub.GetNextResult()
571+
#
572+
# for frame in range(5):
573+
# self.assertEqual({
574+
# 'name': 'i',
575+
# 'value': str(frame),
576+
# 'type': 'int'
577+
# }, python_test_util.PackFrameVariable(result, 'i', frame, 'arguments'))
579578

580579
def testWatchedExpressions(self):
581580

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Unit test for module_explorer module."""
22

3-
# TODO: Get this test to run properly on all supported versions of Python
4-
53
import dis
64
import inspect
75
import os

tests/py/python_breakpoint_test_disabled.py renamed to tests/py/python_breakpoint_test.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""Unit test for python_breakpoint module."""
22

3-
# TODO: Get this test to work with all supported versions of Python.
4-
53
from datetime import datetime
64
from datetime import timedelta
75
import inspect
@@ -12,7 +10,7 @@
1210
from absl.testing import absltest
1311

1412
from googleclouddebugger import cdbg_native as native
15-
from googleclouddebugger import imphook2
13+
from googleclouddebugger import imphook
1614
from googleclouddebugger import python_breakpoint
1715
import python_test_util
1816

@@ -102,7 +100,7 @@ def testDeferredBreakpoint(self):
102100
self._update_queue[0]['stackFrames'][0]['function'])
103101
self.assertTrue(self._update_queue[0]['isFinalState'])
104102

105-
self.assertEmpty(imphook2._import_callbacks)
103+
self.assertEmpty(imphook._import_callbacks)
106104

107105
# Old module search algorithm rejects multiple matches. This test verifies
108106
# that the new module search algorithm searches sys.path sequentially, and
@@ -142,7 +140,7 @@ def testSearchUsingSysPathOrder(self):
142140
self.assertEqual(
143141
'2', self._update_queue[0]['stackFrames'][0]['locals'][0]['value'])
144142

145-
self.assertEmpty(imphook2._import_callbacks)
143+
self.assertEmpty(imphook._import_callbacks)
146144

147145
# Old module search algorithm rejects multiple matches. This test verifies
148146
# that when the new module search cannot find any match in sys.path, it
@@ -183,7 +181,7 @@ def testMultipleDeferredMatches(self):
183181
self.assertEqual(
184182
'1', self._update_queue[0]['stackFrames'][0]['locals'][0]['value'])
185183

186-
self.assertEmpty(imphook2._import_callbacks)
184+
self.assertEmpty(imphook._import_callbacks)
187185

188186
def testNeverLoadedBreakpoint(self):
189187
open(os.path.join(self._test_package_dir, 'never_print.py'), 'w').close()
@@ -223,7 +221,7 @@ def testDeferredNoCodeAtLine(self):
223221
params = desc['parameters']
224222
self.assertIn('defer_empty.py', params[1])
225223
self.assertEqual(params[0], '10')
226-
self.assertEmpty(imphook2._import_callbacks)
224+
self.assertEmpty(imphook._import_callbacks)
227225

228226
def testDeferredBreakpointCancelled(self):
229227
open(os.path.join(self._test_package_dir, 'defer_cancel.py'), 'w').close()
@@ -236,7 +234,7 @@ def testDeferredBreakpointCancelled(self):
236234
breakpoint.Clear()
237235

238236
self.assertFalse(self._completed)
239-
self.assertEmpty(imphook2._import_callbacks)
237+
self.assertEmpty(imphook._import_callbacks)
240238
unused_no_code_line_above = 0 # BPTAG: NO_CODE_LINE_ABOVE
241239

242240
# BPTAG: NO_CODE_LINE
@@ -345,7 +343,7 @@ def testNonRootInitFile(self):
345343
self.assertEqual('DoPrint',
346344
self._update_queue[0]['stackFrames'][0]['function'])
347345

348-
self.assertEmpty(imphook2._import_callbacks)
346+
self.assertEmpty(imphook._import_callbacks)
349347
self._update_queue = []
350348

351349
def testBreakpointInLoadedPackageFile(self):
@@ -414,15 +412,26 @@ def testInvalidCondition(self):
414412
self.assertEqual(set(['BP_ID']), self._completed)
415413
self.assertLen(self._update_queue, 1)
416414
self.assertTrue(self._update_queue[0]['isFinalState'])
417-
self.assertEqual(
418-
{
419-
'isError': True,
420-
'refersTo': 'BREAKPOINT_CONDITION',
421-
'description': {
422-
'format': 'Expression could not be compiled: $0',
423-
'parameters': ['unexpected EOF while parsing']
424-
}
425-
}, self._update_queue[0]['status'])
415+
if sys.version_info.minor < 10:
416+
self.assertEqual(
417+
{
418+
'isError': True,
419+
'refersTo': 'BREAKPOINT_CONDITION',
420+
'description': {
421+
'format': 'Expression could not be compiled: $0',
422+
'parameters': ['unexpected EOF while parsing']
423+
}
424+
}, self._update_queue[0]['status'])
425+
else:
426+
self.assertEqual(
427+
{
428+
'isError': True,
429+
'refersTo': 'BREAKPOINT_CONDITION',
430+
'description': {
431+
'format': 'Expression could not be compiled: $0',
432+
'parameters': ['invalid syntax']
433+
}
434+
}, self._update_queue[0]['status'])
426435

427436
def testHit(self):
428437
breakpoint = python_breakpoint.PythonBreakpoint(self._template, self, self,

0 commit comments

Comments
 (0)