66import tests .debugger .utils as debugger
77
88
9- from utils import scenarios , features , missing_feature , context , irrelevant , bug
9+ from utils import scenarios , features , missing_feature , context , irrelevant , bug , logger
1010from utils .interfaces ._library .miscs import validate_process_tags
1111
1212
@@ -209,6 +209,12 @@ def _validate_snapshots(self):
209209class Test_Debugger_Line_Probe_Snaphots (BaseDebuggerProbeSnaphotTest ):
210210 """Tests for line-level probe snapshots"""
211211
212+ # Default snapshot capture limits
213+ DEFAULT_MAX_REFERENCE_DEPTH = 3
214+ DEFAULT_MAX_COLLECTION_SIZE = 100
215+ DEFAULT_MAX_FIELD_COUNT = 20
216+ DEFAULT_MAX_LENGTH = 255
217+
212218 ### log probe ###
213219 def setup_log_line_snapshot (self ):
214220 self ._setup ("probe_snapshot_log_line" , "/debugger/log" , "log" , lines = None )
@@ -218,6 +224,167 @@ def test_log_line_snapshot(self):
218224 self ._assert ()
219225 self ._validate_snapshots ()
220226
227+ def setup_default_max_reference_depth (self ):
228+ """Setup test for default maxReferenceDepth"""
229+ self ._setup_default_capture_limits ()
230+
231+ def setup_default_max_field_count (self ):
232+ """Setup test for default maxFieldCount"""
233+ self ._setup_default_capture_limits ()
234+
235+ def setup_default_max_collection_size (self ):
236+ """Setup test for default maxCollectionSize"""
237+ self ._setup_default_capture_limits ()
238+
239+ def setup_default_max_length (self ):
240+ """Setup test for default maxLength"""
241+ self ._setup_default_capture_limits ()
242+
243+ def _setup_default_capture_limits (self ):
244+ """Shared setup method for default capture limit tests"""
245+ test_depth = self .DEFAULT_MAX_REFERENCE_DEPTH + 7
246+ test_fields = self .DEFAULT_MAX_FIELD_COUNT + 30
247+ test_collection_size = self .DEFAULT_MAX_COLLECTION_SIZE + 100
248+ test_string_length = self .DEFAULT_MAX_LENGTH + 1000
249+
250+ # Get the line number dynamically based on the language
251+ lines = self .method_and_language_to_line_number ("SnapshotLimits" , context .library .name )
252+
253+ self ._setup (
254+ "probe_snapshot_default_capture_limits" ,
255+ f"/debugger/snapshot/limits?"
256+ f"depth={ test_depth } &"
257+ f"fields={ test_fields } &"
258+ f"collectionSize={ test_collection_size } &"
259+ f"stringLength={ test_string_length } " ,
260+ "log" ,
261+ lines = lines ,
262+ )
263+
264+ def _get_snapshot_locals_variable (self , variable_name : str ) -> dict :
265+ """Helper method to extract a specific local variable from snapshot for default capture limit tests"""
266+ self ._assert ()
267+ self ._validate_snapshots ()
268+
269+ for probe_id in self .probe_ids :
270+ if probe_id not in self .probe_snapshots :
271+ raise ValueError (f"Snapshot { probe_id } was not received." )
272+
273+ snapshots = self .probe_snapshots [probe_id ]
274+ if not snapshots :
275+ raise ValueError (f"No snapshots found for probe { probe_id } " )
276+
277+ snapshot = snapshots [0 ]
278+ debugger_snapshot = snapshot .get ("debugger" , {}).get ("snapshot" ) or snapshot .get ("debugger.snapshot" )
279+
280+ if not debugger_snapshot :
281+ raise ValueError (f"Snapshot data not found in expected format for probe { probe_id } " )
282+ if "captures" not in debugger_snapshot :
283+ raise ValueError (f"No captures found in snapshot for probe { probe_id } " )
284+
285+ captures = debugger_snapshot ["captures" ]
286+ if "lines" in captures :
287+ lines = captures ["lines" ]
288+ if isinstance (lines , dict ) and len (lines ) == 1 :
289+ line_key = next (iter (lines ))
290+ line_data = lines [line_key ]
291+ else :
292+ raise ValueError (f"Expected 'lines' to be a dict with a single key, got: { len (lines )} " )
293+
294+ if line_data and "locals" in line_data :
295+ locals_data = line_data ["locals" ]
296+ assert variable_name in locals_data , f"'{ variable_name } ' is missing from snapshot locals"
297+ return locals_data [variable_name ]
298+
299+ raise ValueError ("No locals data found in snapshot" )
300+
301+ def _measure_captured_depth (self , obj : dict , current_depth : int = 1 ) -> int :
302+ """Measure the actual depth captured before truncation (notCapturedReason='depth')"""
303+ fields = obj ["fields" ]
304+ assert isinstance (fields , dict ), f"Expected 'fields' to be a dict, got: { type (fields )} "
305+ expected_nested_key = "@nested" if context .library .name == "ruby" else "nested"
306+ assert (
307+ expected_nested_key in fields
308+ ), f"Expected '{ expected_nested_key } ' to be present in the 'fields' object, got: { list (fields .keys ())} "
309+ nested = fields [expected_nested_key ]
310+ assert isinstance (nested , dict ), f"Expected 'nested' to be a dict, got: { type (nested )} "
311+
312+ if "notCapturedReason" in nested :
313+ assert (
314+ nested ["notCapturedReason" ] == "depth"
315+ ), f"Expected notCapturedReason to be 'depth', got: { nested ['notCapturedReason' ]} "
316+ return current_depth
317+
318+ return self ._measure_captured_depth (nested , current_depth + 1 )
319+
320+ @bug (
321+ context .library .name == "nodejs" , reason = "DEBUG-4611"
322+ ) # Node.js does apply the correct default, but fails if no `capture` object is present
323+ @bug (
324+ context .library .name == "ruby" , reason = "DEBUG-0000"
325+ ) # TODO: Add real JIRA ticket: Ruby has off-by-one bug: captures 4 levels instead of 3
326+ def test_default_max_reference_depth (self ):
327+ """Test that the tracer uses default maxReferenceDepth=3 when capture property is omitted"""
328+ deep_object = self ._get_snapshot_locals_variable ("deepObject" )
329+ actual_depth = self ._measure_captured_depth (deep_object )
330+ assert (
331+ actual_depth == self .DEFAULT_MAX_REFERENCE_DEPTH
332+ ), f"deepObject should have been captured with { self .DEFAULT_MAX_REFERENCE_DEPTH } levels. Got: { actual_depth } "
333+
334+ @bug (
335+ context .library .name == "nodejs" , reason = "DEBUG-4611"
336+ ) # Node.js does apply the correct default, but fails if no `capture` object is present
337+ def test_default_max_field_count (self ):
338+ """Test that the tracer uses default maxFieldCount=20 when capture property is omitted"""
339+ many_fields = self ._get_snapshot_locals_variable ("manyFields" )
340+ assert (
341+ many_fields .get ("notCapturedReason" ) == "fieldCount"
342+ ), f"manyFields should have notCapturedReason='fieldCount'. Got: { many_fields .get ('notCapturedReason' )} "
343+
344+ captured_count = len (many_fields ["fields" ])
345+ assert (
346+ captured_count == self .DEFAULT_MAX_FIELD_COUNT
347+ ), f"manyFields should have exactly { self .DEFAULT_MAX_FIELD_COUNT } fields captured. Got: { captured_count } "
348+
349+ @bug (
350+ context .library .name == "nodejs" , reason = "DEBUG-4611"
351+ ) # Node.js does apply the correct default, but fails if no `capture` object is present
352+ def test_default_max_collection_size (self ):
353+ """Test that the tracer uses default maxCollectionSize=100 when capture property is omitted"""
354+ large_collection = self ._get_snapshot_locals_variable ("largeCollection" )
355+ assert (
356+ large_collection .get ("notCapturedReason" ) == "collectionSize"
357+ ), f"largeCollection should have notCapturedReason='collectionSize'. Got: { large_collection .get ('notCapturedReason' )} "
358+
359+ actual_size = large_collection .get ("size" )
360+ if isinstance (actual_size , str ) and context .library .name == "java" :
361+ # TODO: Create JIRA ticket: Java size property is a string! Expected an int
362+ logger .warning ("size property is a string! Expected an int" )
363+ actual_size = int (actual_size )
364+ expected_collection_size = self .DEFAULT_MAX_COLLECTION_SIZE + 100
365+ assert (
366+ actual_size == expected_collection_size
367+ ), f"largeCollection should report size={ expected_collection_size } . Got: { actual_size } "
368+
369+ captured_count = len (large_collection ["elements" ])
370+ assert (
371+ captured_count == self .DEFAULT_MAX_COLLECTION_SIZE
372+ ), f"largeCollection should have exactly { self .DEFAULT_MAX_COLLECTION_SIZE } elements. Got: { captured_count } "
373+
374+ @bug (
375+ context .library .name == "nodejs" , reason = "DEBUG-4611"
376+ ) # Node.js does apply the correct default, but fails if no `capture` object is present
377+ @bug (
378+ context .library .name == "dotnet" , reason = "DEBUG-0000"
379+ ) # TODO: Add real JIRA ticket: .NET uses a different default maxLength (1000)
380+ def test_default_max_length (self ):
381+ """Test that the tracer uses default maxLength=255 when capture property is omitted"""
382+ long_string = self ._get_snapshot_locals_variable ("longString" )
383+ string_value = long_string ["value" ]
384+ assert (
385+ len (string_value ) <= self .DEFAULT_MAX_LENGTH
386+ ), f"longString should have length { self .DEFAULT_MAX_LENGTH } . Got: { len (string_value )} "
387+
221388 def setup_log_line_snapshot_debug_track (self ):
222389 self .use_debugger_endpoint = True
223390 self ._setup ("probe_snapshot_log_line" , "/debugger/log" , "log" , lines = None )
0 commit comments