[Concept,10/18] buildman: Move size-display methods to ResultHandler

Message ID 20260110200828.168672-11-sjg@u-boot.org
State New
Headers
Series buildman: Split up the enormous Builder class |

Commit Message

Simon Glass Jan. 10, 2026, 8:08 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Move the size-display methods from Builder to ResultHandler:
- print_size_summary(): Print architecture-level size summaries
- print_size_detail(): Print board-level size details
- print_func_size_detail(): Print function-level bloat analysis
- calc_size_changes(): Calculate size changes across boards
- calc_image_size_changes(): Calculate per-image size changes
- colour_num(): Format numbers with colour

Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 tools/buildman/builder.py       | 262 +-----------------------------
 tools/buildman/main.py          |   1 +
 tools/buildman/resulthandler.py | 280 +++++++++++++++++++++++++++++++-
 tools/buildman/test_builder.py  |  16 +-
 4 files changed, 289 insertions(+), 270 deletions(-)
  

Patch

diff --git a/tools/buildman/builder.py b/tools/buildman/builder.py
index db8d980e83e..4b6106b881b 100644
--- a/tools/buildman/builder.py
+++ b/tools/buildman/builder.py
@@ -24,6 +24,7 @@  from buildman.cfgutil import Config, process_config
 from buildman.outcome import (BoardStatus, DisplayOptions, ErrLine, Outcome,
                               OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR,
                               OUTCOME_UNKNOWN)
+from buildman.resulthandler import ResultHandler
 from u_boot_pylib import command
 from u_boot_pylib import gitutil
 from u_boot_pylib import terminal
@@ -1040,21 +1041,6 @@  class Builder:
             else:
                 arch_list[arch] += text
 
-
-    def colour_num(self, num):
-        """Format a number with colour depending on its value
-
-        Args:
-            num (int): Number to format
-
-        Returns:
-            str: Formatted string (red if positive, green if negative/zero)
-        """
-        color = self.col.RED if num > 0 else self.col.GREEN
-        if num == 0:
-            return '0'
-        return self.col.build(color, str(num))
-
     def reset_result_summary(self, board_selected):
         """Reset the results summary ready for use.
 
@@ -1078,247 +1064,6 @@  class Builder:
         self._base_config = None
         self._base_environment = None
 
-    def print_func_size_detail(self, fname, old, new):
-        """Print detailed size information for each function
-
-        Args:
-            fname (str): Filename to print (e.g. 'u-boot')
-            old (dict): Dictionary of old function sizes, keyed by function name
-            new (dict): Dictionary of new function sizes, keyed by function name
-        """
-        grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
-        delta, common = [], {}
-
-        for a in old:
-            if a in new:
-                common[a] = 1
-
-        for name in old:
-            if name not in common:
-                remove += 1
-                down += old[name]
-                delta.append([-old[name], name])
-
-        for name in new:
-            if name not in common:
-                add += 1
-                up += new[name]
-                delta.append([new[name], name])
-
-        for name in common:
-            diff = new.get(name, 0) - old.get(name, 0)
-            if diff > 0:
-                grow, up = grow + 1, up + diff
-            elif diff < 0:
-                shrink, down = shrink + 1, down - diff
-            delta.append([diff, name])
-
-        delta.sort()
-        delta.reverse()
-
-        args = [add, -remove, grow, -shrink, up, -down, up - down]
-        if max(args) == 0 and min(args) == 0:
-            return
-        args = [self.colour_num(x) for x in args]
-        indent = ' ' * 15
-        tprint(f'{indent}{self.col.build(self.col.YELLOW, fname)}: add: '
-               f'{args[0]}/{args[1]}, grow: {args[2]}/{args[3]} bytes: '
-               f'{args[4]}/{args[5]} ({args[6]})')
-        tprint(f'{indent}  {"function":<38s} {"old":>7s} {"new":>7s} '
-               f'{"delta":>7s}')
-        for diff, name in delta:
-            if diff:
-                color = self.col.RED if diff > 0 else self.col.GREEN
-                msg = (f'{indent}  {name:<38s} {old.get(name, "-"):>7} '
-                       f'{new.get(name, "-"):>7} {diff:+7d}')
-                tprint(msg, colour=color)
-
-
-    def print_size_detail(self, target_list, show_bloat):
-        """Show details size information for each board
-
-        Args:
-            target_list (list): List of targets, each a dict containing:
-                    'target': Target name
-                    'total_diff': Total difference in bytes across all areas
-                    <part_name>: Difference for that part
-            show_bloat (bool): Show detail for each function
-        """
-        targets_by_diff = sorted(target_list, reverse=True,
-        key=lambda x: x['_total_diff'])
-        for result in targets_by_diff:
-            printed_target = False
-            for name in sorted(result):
-                diff = result[name]
-                if name.startswith('_'):
-                    continue
-                colour = self.col.RED if diff > 0 else self.col.GREEN
-                msg = f' {name} {diff:+d}'
-                if not printed_target:
-                    tprint(f'{"":10s}  {result["_target"]:<15s}:',
-                          newline=False)
-                    printed_target = True
-                tprint(msg, colour=colour, newline=False)
-            if printed_target:
-                tprint()
-                if show_bloat:
-                    target = result['_target']
-                    outcome = result['_outcome']
-                    base_outcome = self._base_board_dict[target]
-                    for fname in outcome.func_sizes:
-                        self.print_func_size_detail(fname,
-                                                 base_outcome.func_sizes[fname],
-                                                 outcome.func_sizes[fname])
-
-
-    @staticmethod
-    def _calc_image_size_changes(target, sizes, base_sizes):
-        """Calculate size changes for each image/part
-
-        Args:
-            target (str): Target board name
-            sizes (dict): Dict of image sizes, keyed by image name
-            base_sizes (dict): Dict of base image sizes, keyed by image name
-
-        Returns:
-            dict: Size changes, e.g.:
-                {'_target': 'snapper9g45', 'data': 5, 'u-boot-spl:text': -4}
-                meaning U-Boot data increased by 5 bytes, SPL text decreased
-                by 4
-        """
-        err = {'_target' : target}
-        for image in sizes:
-            if image in base_sizes:
-                base_image = base_sizes[image]
-                # Loop through the text, data, bss parts
-                for part in sorted(sizes[image]):
-                    diff = sizes[image][part] - base_image[part]
-                    if diff:
-                        if image == 'u-boot':
-                            name = part
-                        else:
-                            name = image + ':' + part
-                        err[name] = diff
-        return err
-
-    def _calc_size_changes(self, board_selected, board_dict):
-        """Calculate changes in size for different image parts
-
-        The previous sizes are in Board.sizes, for each board
-
-        Args:
-            board_selected (dict): Dict containing boards to summarise, keyed
-                by board.target
-            board_dict (dict): Dict containing boards for which we built this
-                commit, keyed by board.target. The value is an Outcome object.
-
-        Returns:
-            tuple: (arch_list, arch_count) where:
-                arch_list: dict keyed by arch name, containing a list of
-                    size-change dicts
-                arch_count: dict keyed by arch name, containing the number of
-                    boards for that arch
-        """
-        arch_list = {}
-        arch_count = {}
-        for target in board_dict:
-            if target not in board_selected:
-                continue
-            base_sizes = self._base_board_dict[target].sizes
-            outcome = board_dict[target]
-            sizes = outcome.sizes
-            err = self._calc_image_size_changes(target, sizes, base_sizes)
-            arch = board_selected[target].arch
-            if not arch in arch_count:
-                arch_count[arch] = 1
-            else:
-                arch_count[arch] += 1
-            if not sizes:
-                pass    # Only add to our list when we have some stats
-            elif not arch in arch_list:
-                arch_list[arch] = [err]
-            else:
-                arch_list[arch].append(err)
-        return arch_list, arch_count
-
-    def print_size_summary(self, board_selected, board_dict, show_detail,
-                         show_bloat):
-        """Print a summary of image sizes broken down by section.
-
-        The summary takes the form of one line per architecture. The
-        line contains deltas for each of the sections (+ means the section
-        got bigger, - means smaller). The numbers are the average number
-        of bytes that a board in this section increased by.
-
-        For example:
-           powerpc: (622 boards)   text -0.0
-          arm: (285 boards)   text -0.0
-
-        Args:
-            board_selected (dict): Dict containing boards to summarise, keyed
-                by board.target
-            board_dict (dict): Dict containing boards for which we built this
-                commit, keyed by board.target. The value is an Outcome object.
-            show_detail (bool): Show size delta detail for each board
-            show_bloat (bool): Show detail for each function
-        """
-        arch_list, arch_count = self._calc_size_changes(board_selected,
-                                                        board_dict)
-
-        # We now have a list of image size changes sorted by arch
-        # Print out a summary of these
-        for arch, target_list in arch_list.items():
-            # Get total difference for each type
-            totals = {}
-            for result in target_list:
-                total = 0
-                for name, diff in result.items():
-                    if name.startswith('_'):
-                        continue
-                    total += diff
-                    if name in totals:
-                        totals[name] += diff
-                    else:
-                        totals[name] = diff
-                result['_total_diff'] = total
-                result['_outcome'] = board_dict[result['_target']]
-
-            self._print_arch_size_summary(arch, target_list, arch_count,
-                                          totals, show_detail, show_bloat)
-
-    def _print_arch_size_summary(self, arch, target_list, arch_count, totals,
-                                 show_detail, show_bloat):
-        """Print size summary for a single architecture
-
-        Args:
-            arch (str): Architecture name
-            target_list (list): List of size-change dicts for this arch
-            arch_count (dict): Dict of arch name to board count
-            totals (dict): Dict of name to total size diff
-            show_detail (bool): Show size delta detail for each board
-            show_bloat (bool): Show detail for each function
-        """
-        count = len(target_list)
-        printed_arch = False
-        for name in sorted(totals):
-            diff = totals[name]
-            if diff:
-                # Display the average difference in this name for this
-                # architecture
-                avg_diff = float(diff) / count
-                color = self.col.RED if avg_diff > 0 else self.col.GREEN
-                msg = f' {name} {avg_diff:+1.1f}'
-                if not printed_arch:
-                    tprint(f'{arch:>10s}: (for {count}/{arch_count[arch]} '
-                           'boards)', newline=False)
-                    printed_arch = True
-                tprint(msg, colour=color, newline=False)
-
-        if printed_arch:
-            tprint()
-            if show_detail:
-                self.print_size_detail(target_list, show_bloat)
-
     def _classify_boards(self, board_selected, board_dict):
         """Classify boards into outcome categories
 
@@ -1789,8 +1534,9 @@  class Builder:
                                    worse_err, better_warn, worse_warn)
 
         if show_sizes:
-            self.print_size_summary(board_selected, board_dict, show_detail,
-                                  show_bloat)
+            self._result_handler.print_size_summary(
+                board_selected, board_dict, self._base_board_dict,
+                show_detail, show_bloat)
 
         if show_environment and self._base_environment:
             self._show_environment_changes(board_selected, board_dict,
diff --git a/tools/buildman/main.py b/tools/buildman/main.py
index c8502c370f9..af289a46508 100755
--- a/tools/buildman/main.py
+++ b/tools/buildman/main.py
@@ -79,6 +79,7 @@  def run_test_coverage():
                         'tools/buildman/builderthread.py',
                         'tools/buildman/cfgutil.py',
                         'tools/buildman/control.py',
+                        'tools/buildman/resulthandler.py',
                         'tools/buildman/toolchain.py'])
 
 
diff --git a/tools/buildman/resulthandler.py b/tools/buildman/resulthandler.py
index 86f95f3eea5..4c9b3c2f9cc 100644
--- a/tools/buildman/resulthandler.py
+++ b/tools/buildman/resulthandler.py
@@ -1,14 +1,23 @@ 
 # SPDX-License-Identifier: GPL-2.0+
 # Copyright (c) 2013 The Chromium OS Authors.
+#
+# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
+#
 
-"""Result handler for buildman build results"""
+"""Result writer for buildman build results"""
+
+from u_boot_pylib.terminal import tprint
 
 
 class ResultHandler:
-    """Handles display of build results and summaries
+    """Handles display of build size results and summaries
+
+    This class is responsible for displaying size information from builds,
+    including per-architecture summaries, per-board details, and per-function
+    bloat analysis.
 
-    This class is responsible for displaying build results, including
-    size information, errors, warnings, and configuration changes.
+    Attributes:
+        col: terminal.Color object for coloured output
     """
 
     def __init__(self, col, opts):
@@ -29,3 +38,266 @@  class ResultHandler:
             builder (Builder): Builder object to use for getting results
         """
         self._builder = builder
+
+    def colour_num(self, num):
+        """Format a number with colour depending on its value
+
+        Args:
+            num (int): Number to format
+
+        Returns:
+            str: Formatted string (red if positive, green if negative/zero)
+        """
+        color = self._col.RED if num > 0 else self._col.GREEN
+        if num == 0:
+            return '0'
+        return self._col.build(color, str(num))
+
+    def print_func_size_detail(self, fname, old, new):
+        """Print detailed size information for each function
+
+        Args:
+            fname (str): Filename to print (e.g. 'u-boot')
+            old (dict): Dictionary of old function sizes, keyed by function name
+            new (dict): Dictionary of new function sizes, keyed by function name
+        """
+        grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
+        delta, common = [], {}
+
+        for a in old:
+            if a in new:
+                common[a] = 1
+
+        for name in old:
+            if name not in common:
+                remove += 1
+                down += old[name]
+                delta.append([-old[name], name])
+
+        for name in new:
+            if name not in common:
+                add += 1
+                up += new[name]
+                delta.append([new[name], name])
+
+        for name in common:
+            diff = new.get(name, 0) - old.get(name, 0)
+            if diff > 0:
+                grow, up = grow + 1, up + diff
+            elif diff < 0:
+                shrink, down = shrink + 1, down - diff
+            delta.append([diff, name])
+
+        delta.sort()
+        delta.reverse()
+
+        args = [add, -remove, grow, -shrink, up, -down, up - down]
+        if max(args) == 0 and min(args) == 0:
+            return
+        args = [self.colour_num(x) for x in args]
+        indent = ' ' * 15
+        tprint(f'{indent}{self._col.build(self._col.YELLOW, fname)}: add: '
+               f'{args[0]}/{args[1]}, grow: {args[2]}/{args[3]} bytes: '
+               f'{args[4]}/{args[5]} ({args[6]})')
+        tprint(f'{indent}  {"function":<38s} {"old":>7s} {"new":>7s} '
+               f'{"delta":>7s}')
+        for diff, name in delta:
+            if diff:
+                color = self._col.RED if diff > 0 else self._col.GREEN
+                msg = (f'{indent}  {name:<38s} {old.get(name, "-"):>7} '
+                       f'{new.get(name, "-"):>7} {diff:+7d}')
+                tprint(msg, colour=color)
+
+    def print_size_detail(self, target_list, base_board_dict, board_dict,
+                          show_bloat):
+        """Show detailed size information for each board
+
+        Args:
+            target_list (list): List of targets, each a dict containing:
+                    'target': Target name
+                    'total_diff': Total difference in bytes across all areas
+                    <part_name>: Difference for that part
+            base_board_dict (dict): Dict of base board outcomes
+            board_dict (dict): Dict of current board outcomes
+            show_bloat (bool): Show detail for each function
+        """
+        targets_by_diff = sorted(target_list, reverse=True,
+        key=lambda x: x['_total_diff'])
+        for result in targets_by_diff:
+            printed_target = False
+            for name in sorted(result):
+                diff = result[name]
+                if name.startswith('_'):
+                    continue
+                colour = self._col.RED if diff > 0 else self._col.GREEN
+                msg = f' {name} {diff:+d}'
+                if not printed_target:
+                    tprint(f'{"":10s}  {result["_target"]:<15s}:',
+                          newline=False)
+                    printed_target = True
+                tprint(msg, colour=colour, newline=False)
+            if printed_target:
+                tprint()
+                if show_bloat:
+                    target = result['_target']
+                    outcome = board_dict[target]
+                    base_outcome = base_board_dict[target]
+                    for fname in outcome.func_sizes:
+                        self.print_func_size_detail(fname,
+                                                 base_outcome.func_sizes[fname],
+                                                 outcome.func_sizes[fname])
+
+    @staticmethod
+    def calc_image_size_changes(target, sizes, base_sizes):
+        """Calculate size changes for each image/part
+
+        Args:
+            target (str): Target board name
+            sizes (dict): Dict of image sizes, keyed by image name
+            base_sizes (dict): Dict of base image sizes, keyed by image name
+
+        Returns:
+            dict: Size changes, e.g.:
+                {'_target': 'snapper9g45', 'data': 5, 'u-boot-spl:text': -4}
+                meaning U-Boot data increased by 5 bytes, SPL text decreased
+                by 4
+        """
+        err = {'_target' : target}
+        for image in sizes:
+            if image in base_sizes:
+                base_image = base_sizes[image]
+                # Loop through the text, data, bss parts
+                for part in sorted(sizes[image]):
+                    diff = sizes[image][part] - base_image[part]
+                    if diff:
+                        if image == 'u-boot':
+                            name = part
+                        else:
+                            name = image + ':' + part
+                        err[name] = diff
+        return err
+
+    def calc_size_changes(self, board_selected, board_dict, base_board_dict):
+        """Calculate changes in size for different image parts
+
+        The previous sizes are in Board.sizes, for each board
+
+        Args:
+            board_selected (dict): Dict containing boards to summarise, keyed
+                by board.target
+            board_dict (dict): Dict containing boards for which we built this
+                commit, keyed by board.target. The value is an Outcome object.
+            base_board_dict (dict): Dict of base board outcomes
+
+        Returns:
+            tuple: (arch_list, arch_count) where:
+                arch_list: dict keyed by arch name, containing a list of
+                    size-change dicts
+                arch_count: dict keyed by arch name, containing the number of
+                    boards for that arch
+        """
+        arch_list = {}
+        arch_count = {}
+        for target in board_dict:
+            if target not in board_selected:
+                continue
+            base_sizes = base_board_dict[target].sizes
+            outcome = board_dict[target]
+            sizes = outcome.sizes
+            err = self.calc_image_size_changes(target, sizes, base_sizes)
+            arch = board_selected[target].arch
+            if not arch in arch_count:
+                arch_count[arch] = 1
+            else:
+                arch_count[arch] += 1
+            if not sizes:
+                pass    # Only add to our list when we have some stats
+            elif not arch in arch_list:
+                arch_list[arch] = [err]
+            else:
+                arch_list[arch].append(err)
+        return arch_list, arch_count
+
+    def print_size_summary(self, board_selected, board_dict, base_board_dict,
+                           show_detail, show_bloat):
+        """Print a summary of image sizes broken down by section.
+
+        The summary takes the form of one line per architecture. The
+        line contains deltas for each of the sections (+ means the section
+        got bigger, - means smaller). The numbers are the average number
+        of bytes that a board in this section increased by.
+
+        For example:
+           powerpc: (622 boards)   text -0.0
+          arm: (285 boards)   text -0.0
+
+        Args:
+            board_selected (dict): Dict containing boards to summarise, keyed
+                by board.target
+            board_dict (dict): Dict containing boards for which we built this
+                commit, keyed by board.target. The value is an Outcome object.
+            base_board_dict (dict): Dict of base board outcomes
+            show_detail (bool): Show size delta detail for each board
+            show_bloat (bool): Show detail for each function
+        """
+        arch_list, arch_count = self.calc_size_changes(board_selected,
+                                                       board_dict,
+                                                       base_board_dict)
+
+        # We now have a list of image size changes sorted by arch
+        # Print out a summary of these
+        for arch, target_list in arch_list.items():
+            # Get total difference for each type
+            totals = {}
+            for result in target_list:
+                total = 0
+                for name, diff in result.items():
+                    if name.startswith('_'):
+                        continue
+                    total += diff
+                    if name in totals:
+                        totals[name] += diff
+                    else:
+                        totals[name] = diff
+                result['_total_diff'] = total
+
+            self._print_arch_size_summary(arch, target_list, arch_count,
+                                          totals, base_board_dict, board_dict,
+                                          show_detail, show_bloat)
+
+    def _print_arch_size_summary(self, arch, target_list, arch_count, totals,
+                                 base_board_dict, board_dict,
+                                 show_detail, show_bloat):
+        """Print size summary for a single architecture
+
+        Args:
+            arch (str): Architecture name
+            target_list (list): List of size-change dicts for this arch
+            arch_count (dict): Dict of arch name to board count
+            totals (dict): Dict of name to total size diff
+            base_board_dict (dict): Dict of base board outcomes
+            board_dict (dict): Dict of current board outcomes
+            show_detail (bool): Show size delta detail for each board
+            show_bloat (bool): Show detail for each function
+        """
+        count = len(target_list)
+        printed_arch = False
+        for name in sorted(totals):
+            diff = totals[name]
+            if diff:
+                # Display the average difference in this name for this
+                # architecture
+                avg_diff = float(diff) / count
+                color = self._col.RED if avg_diff > 0 else self._col.GREEN
+                msg = f' {name} {avg_diff:+1.1f}'
+                if not printed_arch:
+                    tprint(f'{arch:>10s}: (for {count}/{arch_count[arch]} '
+                           'boards)', newline=False)
+                    printed_arch = True
+                tprint(msg, colour=color, newline=False)
+
+        if printed_arch:
+            tprint()
+            if show_detail:
+                self.print_size_detail(target_list, base_board_dict, board_dict,
+                                       show_bloat)
diff --git a/tools/buildman/test_builder.py b/tools/buildman/test_builder.py
index 40132c1b46f..69e9e324c53 100644
--- a/tools/buildman/test_builder.py
+++ b/tools/buildman/test_builder.py
@@ -20,7 +20,7 @@  from u_boot_pylib import terminal
 
 
 class TestPrintFuncSizeDetail(unittest.TestCase):
-    """Tests for Builder.print_func_size_detail()"""
+    """Tests for ResultHandler.print_func_size_detail()"""
 
     def setUp(self):
         """Set up test fixtures"""
@@ -46,7 +46,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
         new = {'func_a': 100, 'func_b': 200}
 
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', old, new)
+        self.result_handler.print_func_size_detail('u-boot', old, new)
         lines = terminal.get_print_test_lines()
 
         # No output when there are no changes
@@ -58,7 +58,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
         new = {'func_a': 150}
 
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', old, new)
+        self.result_handler.print_func_size_detail('u-boot', old, new)
         lines = terminal.get_print_test_lines()
 
         text = '\n'.join(line.text for line in lines)
@@ -76,7 +76,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
         new = {'func_a': 150}
 
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', old, new)
+        self.result_handler.print_func_size_detail('u-boot', old, new)
         lines = terminal.get_print_test_lines()
 
         text = '\n'.join(line.text for line in lines)
@@ -89,7 +89,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
         new = {'func_a': 100, 'func_b': 200}
 
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', old, new)
+        self.result_handler.print_func_size_detail('u-boot', old, new)
         lines = terminal.get_print_test_lines()
 
         text = '\n'.join(line.text for line in lines)
@@ -105,7 +105,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
         new = {'func_a': 100}
 
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', old, new)
+        self.result_handler.print_func_size_detail('u-boot', old, new)
         lines = terminal.get_print_test_lines()
 
         text = '\n'.join(line.text for line in lines)
@@ -129,7 +129,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
         }
 
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', old, new)
+        self.result_handler.print_func_size_detail('u-boot', old, new)
         lines = terminal.get_print_test_lines()
 
         text = '\n'.join(line.text for line in lines)
@@ -148,7 +148,7 @@  class TestPrintFuncSizeDetail(unittest.TestCase):
     def test_empty_dicts(self):
         """Test with empty dictionaries"""
         terminal.get_print_test_lines()  # Clear
-        self.builder.print_func_size_detail('u-boot', {}, {})
+        self.result_handler.print_func_size_detail('u-boot', {}, {})
         lines = terminal.get_print_test_lines()
 
         # No output when both dicts are empty