@@ -272,25 +272,26 @@ def test_collapsed_stack_collector_with_empty_and_deep_stacks(self):
272272
273273 # Test with empty frames
274274 collector .collect ([])
275- self .assertEqual (len (collector .call_trees ), 0 )
275+ self .assertEqual (len (collector .stack_counter ), 0 )
276276
277277 # Test with single frame stack
278278 test_frames = [MockInterpreterInfo (0 , [MockThreadInfo (1 , [("file.py" , 10 , "func" )])])]
279279 collector .collect (test_frames )
280- self .assertEqual (len (collector .call_trees ), 1 )
281- self .assertEqual (collector .call_trees [0 ], [("file.py" , 10 , "func" )])
280+ self .assertEqual (len (collector .stack_counter ), 1 )
281+ ((path ,), count ), = collector .stack_counter .items ()
282+ self .assertEqual (path , ("file.py" , 10 , "func" ))
283+ self .assertEqual (count , 1 )
282284
283285 # Test with very deep stack
284286 deep_stack = [(f"file{ i } .py" , i , f"func{ i } " ) for i in range (100 )]
285287 test_frames = [MockInterpreterInfo (0 , [MockThreadInfo (1 , deep_stack )])]
286288 collector = CollapsedStackCollector ()
287289 collector .collect (test_frames )
288- self .assertEqual (len (collector .call_trees [0 ]), 100 )
289- # Check it's properly reversed
290- self .assertEqual (
291- collector .call_trees [0 ][0 ], ("file99.py" , 99 , "func99" )
292- )
293- self .assertEqual (collector .call_trees [0 ][- 1 ], ("file0.py" , 0 , "func0" ))
290+ # One aggregated path with 100 frames (reversed)
291+ (path_tuple ,), = (collector .stack_counter .keys (),)
292+ self .assertEqual (len (path_tuple ), 100 )
293+ self .assertEqual (path_tuple [0 ], ("file99.py" , 99 , "func99" ))
294+ self .assertEqual (path_tuple [- 1 ], ("file0.py" , 0 , "func0" ))
294295
295296 def test_pstats_collector_basic (self ):
296297 """Test basic PstatsCollector functionality."""
@@ -382,27 +383,20 @@ def test_collapsed_stack_collector_basic(self):
382383 collector = CollapsedStackCollector ()
383384
384385 # Test empty state
385- self .assertEqual (len (collector .call_trees ), 0 )
386- self .assertEqual (len (collector .function_samples ), 0 )
386+ self .assertEqual (len (collector .stack_counter ), 0 )
387387
388388 # Test collecting sample data
389389 test_frames = [
390390 MockInterpreterInfo (0 , [MockThreadInfo (1 , [("file.py" , 10 , "func1" ), ("file.py" , 20 , "func2" )])])
391391 ]
392392 collector .collect (test_frames )
393393
394- # Should store call tree (reversed)
395- self .assertEqual (len (collector .call_trees ), 1 )
396- expected_tree = [("file.py" , 20 , "func2" ), ("file.py" , 10 , "func1" )]
397- self .assertEqual (collector .call_trees [0 ], expected_tree )
398-
399- # Should count function samples
400- self .assertEqual (
401- collector .function_samples [("file.py" , 10 , "func1" )], 1
402- )
403- self .assertEqual (
404- collector .function_samples [("file.py" , 20 , "func2" )], 1
405- )
394+ # Should store one reversed path
395+ self .assertEqual (len (collector .stack_counter ), 1 )
396+ (path , count ), = collector .stack_counter .items ()
397+ expected_tree = (("file.py" , 20 , "func2" ), ("file.py" , 10 , "func1" ))
398+ self .assertEqual (path , expected_tree )
399+ self .assertEqual (count , 1 )
406400
407401 def test_collapsed_stack_collector_export (self ):
408402 collapsed_out = tempfile .NamedTemporaryFile (delete = False )
@@ -441,9 +435,9 @@ def test_flamegraph_collector_basic(self):
441435 """Test basic FlamegraphCollector functionality."""
442436 collector = FlamegraphCollector ()
443437
444- # Test empty state (inherits from StackTraceCollector)
445- self . assertEqual ( len ( collector .call_trees ), 0 )
446- self .assertEqual ( len ( collector . function_samples ), 0 )
438+ # Empty collector should produce 'No Data'
439+ data = collector ._convert_to_flamegraph_format ( )
440+ self .assertIn ( data [ "name" ], ( "No Data" , "No significant data" ) )
447441
448442 # Test collecting sample data
449443 test_frames = [
@@ -454,18 +448,18 @@ def test_flamegraph_collector_basic(self):
454448 ]
455449 collector .collect (test_frames )
456450
457- # Should store call tree (reversed)
458- self . assertEqual ( len ( collector .call_trees ), 1 )
459- expected_tree = [( "file.py" , 20 , " func2" ), ( "file.py" , 10 , " func1" )]
460- self . assertEqual ( collector . call_trees [ 0 ], expected_tree )
461-
462- # Should count function samples
463- self .assertEqual (
464- collector . function_samples [( "file.py " , 10 , "func1" )], 1
465- )
466- self . assertEqual (
467- collector . function_samples [( " file.py" , 20 , "func2" )], 1
468- )
451+ # Convert and verify structure: func2 -> func1 with counts = 1
452+ data = collector ._convert_to_flamegraph_format ( )
453+ # Expect promotion: root is the single child ( func2), with func1 as its only child
454+ name = data . get ( "name" , "" )
455+ self . assertIsInstance ( name , str )
456+ self . assertTrue ( name . startswith ( "Program Root: " ))
457+ self .assertIn ( "func2 (file.py:20)" , name ) # formatted name
458+ children = data . get ( "children " , [])
459+ self . assertEqual ( len ( children ), 1 )
460+ child = children [ 0 ]
461+ self . assertIn ( "func1 ( file.py:10) " , child [ "name" ]) # formatted name
462+ self . assertEqual ( child [ "value" ], 1 )
469463
470464 def test_flamegraph_collector_export (self ):
471465 """Test flamegraph HTML export functionality."""
@@ -1508,28 +1502,29 @@ def test_collapsed_stack_with_recursion(self):
15081502 for frames in recursive_frames :
15091503 collector .collect ([frames ])
15101504
1511- # Should capture both call trees
1512- self .assertEqual (len (collector .call_trees ), 2 )
1513-
1514- # First tree should be longer (deeper recursion)
1515- tree1 = collector .call_trees [0 ]
1516- tree2 = collector .call_trees [1 ]
1505+ # Should capture both call paths
1506+ self .assertEqual (len (collector .stack_counter ), 2 )
15171507
1518- # Trees should be different lengths due to different recursion depths
1519- self .assertNotEqual (len (tree1 ), len (tree2 ))
1508+ # First path should be longer (deeper recursion) than the second
1509+ paths = list (collector .stack_counter .keys ())
1510+ lengths = [len (p ) for p in paths ]
1511+ self .assertNotEqual (lengths [0 ], lengths [1 ])
15201512
15211513 # Both should contain factorial calls
1522- self .assertTrue (any ("factorial" in str (frame ) for frame in tree1 ))
1523- self .assertTrue (any ("factorial" in str (frame ) for frame in tree2 ))
1514+ self .assertTrue (any (any (f [2 ] == "factorial" for f in p ) for p in paths ))
15241515
1525- # Function samples should count all occurrences
1516+ # Verify total occurrences via aggregation
15261517 factorial_key = ("factorial.py" , 10 , "factorial" )
15271518 main_key = ("main.py" , 5 , "main" )
15281519
1529- # factorial appears 5 times total (3 + 2)
1530- self .assertEqual (collector .function_samples [factorial_key ], 5 )
1531- # main appears 2 times total
1532- self .assertEqual (collector .function_samples [main_key ], 2 )
1520+ def total_occurrences (func ):
1521+ total = 0
1522+ for path , count in collector .stack_counter .items ():
1523+ total += sum (1 for f in path if f == func ) * count
1524+ return total
1525+
1526+ self .assertEqual (total_occurrences (factorial_key ), 5 )
1527+ self .assertEqual (total_occurrences (main_key ), 2 )
15331528
15341529
15351530@requires_subprocess ()
0 commit comments