blob: 33f9373b8f8ac6b389a91ac030331528fd9c866e [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassc05694f2013-04-03 11:07:16 +00002# Copyright (c) 2013 The Chromium OS Authors.
3#
4# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
5#
Simon Glassc05694f2013-04-03 11:07:16 +00006
7import collections
Simon Glassc05694f2013-04-03 11:07:16 +00008from datetime import datetime, timedelta
9import glob
10import os
11import re
Simon Glassc78ed662019-10-31 07:42:53 -060012import queue
Simon Glassc05694f2013-04-03 11:07:16 +000013import shutil
Simon Glass205ac042016-09-18 16:48:37 -060014import signal
Simon Glassc05694f2013-04-03 11:07:16 +000015import string
16import sys
Simon Glassd26e1442016-09-18 16:48:35 -060017import threading
Simon Glassc05694f2013-04-03 11:07:16 +000018import time
19
Simon Glassf0d9c102020-04-17 18:09:02 -060020from buildman import builderthread
21from buildman import toolchain
Simon Glassa997ea52020-04-17 18:09:04 -060022from patman import command
23from patman import gitutil
24from patman import terminal
Simon Glass02811582022-01-29 14:14:18 -070025from patman.terminal import tprint
Simon Glassc05694f2013-04-03 11:07:16 +000026
Simon Glass146b6022021-10-19 21:43:24 -060027# This indicates an new int or hex Kconfig property with no default
28# It hangs the build since the 'conf' tool cannot proceed without valid input.
29#
30# We get a repeat sequence of something like this:
31# >>
32# Break things (BREAK_ME) [] (NEW)
33# Error in reading or end of file.
34# <<
35# which indicates that BREAK_ME has an empty default
36RE_NO_DEFAULT = re.compile(b'\((\w+)\) \[] \(NEW\)')
37
Simon Glassc05694f2013-04-03 11:07:16 +000038"""
39Theory of Operation
40
41Please see README for user documentation, and you should be familiar with
42that before trying to make sense of this.
43
44Buildman works by keeping the machine as busy as possible, building different
45commits for different boards on multiple CPUs at once.
46
47The source repo (self.git_dir) contains all the commits to be built. Each
48thread works on a single board at a time. It checks out the first commit,
49configures it for that board, then builds it. Then it checks out the next
50commit and builds it (typically without re-configuring). When it runs out
51of commits, it gets another job from the builder and starts again with that
52board.
53
54Clearly the builder threads could work either way - they could check out a
55commit and then built it for all boards. Using separate directories for each
56commit/board pair they could leave their build product around afterwards
57also.
58
59The intent behind building a single board for multiple commits, is to make
60use of incremental builds. Since each commit is built incrementally from
61the previous one, builds are faster. Reconfiguring for a different board
62removes all intermediate object files.
63
64Many threads can be working at once, but each has its own working directory.
65When a thread finishes a build, it puts the output files into a result
66directory.
67
68The base directory used by buildman is normally '../<branch>', i.e.
69a directory higher than the source repository and named after the branch
70being built.
71
72Within the base directory, we have one subdirectory for each commit. Within
73that is one subdirectory for each board. Within that is the build output for
74that commit/board combination.
75
76Buildman also create working directories for each thread, in a .bm-work/
77subdirectory in the base dir.
78
79As an example, say we are building branch 'us-net' for boards 'sandbox' and
80'seaboard', and say that us-net has two commits. We will have directories
81like this:
82
83us-net/ base directory
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +030084 01_g4ed4ebc_net--Add-tftp-speed-/
Simon Glassc05694f2013-04-03 11:07:16 +000085 sandbox/
86 u-boot.bin
87 seaboard/
88 u-boot.bin
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +030089 02_g4ed4ebc_net--Check-tftp-comp/
Simon Glassc05694f2013-04-03 11:07:16 +000090 sandbox/
91 u-boot.bin
92 seaboard/
93 u-boot.bin
94 .bm-work/
95 00/ working directory for thread 0 (contains source checkout)
96 build/ build output
97 01/ working directory for thread 1
98 build/ build output
99 ...
100u-boot/ source directory
101 .git/ repository
102"""
103
Simon Glassde0fefc2020-04-09 15:08:36 -0600104"""Holds information about a particular error line we are outputing
105
106 char: Character representation: '+': error, '-': fixed error, 'w+': warning,
107 'w-' = fixed warning
108 boards: List of Board objects which have line in the error/warning output
109 errline: The text of the error line
110"""
Simon Glass5df45222022-07-11 19:04:00 -0600111ErrLine = collections.namedtuple('ErrLine', 'char,brds,errline')
Simon Glassde0fefc2020-04-09 15:08:36 -0600112
Simon Glassc05694f2013-04-03 11:07:16 +0000113# Possible build outcomes
Simon Glassc78ed662019-10-31 07:42:53 -0600114OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
Simon Glassc05694f2013-04-03 11:07:16 +0000115
Simon Glassd214bef2017-04-12 18:23:26 -0600116# Translate a commit subject into a valid filename (and handle unicode)
Simon Glassc78ed662019-10-31 07:42:53 -0600117trans_valid_chars = str.maketrans('/: ', '---')
Simon Glassc05694f2013-04-03 11:07:16 +0000118
Simon Glasscde5c302016-11-13 14:25:53 -0700119BASE_CONFIG_FILENAMES = [
120 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
121]
122
123EXTRA_CONFIG_FILENAMES = [
Simon Glassdb17fb82015-02-05 22:06:15 -0700124 '.config', '.config-spl', '.config-tpl',
125 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
126 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
Simon Glassdb17fb82015-02-05 22:06:15 -0700127]
128
Simon Glasscad8abf2015-08-25 21:52:14 -0600129class Config:
130 """Holds information about configuration settings for a board."""
Simon Glasscde5c302016-11-13 14:25:53 -0700131 def __init__(self, config_filename, target):
Simon Glasscad8abf2015-08-25 21:52:14 -0600132 self.target = target
133 self.config = {}
Simon Glasscde5c302016-11-13 14:25:53 -0700134 for fname in config_filename:
Simon Glasscad8abf2015-08-25 21:52:14 -0600135 self.config[fname] = {}
136
137 def Add(self, fname, key, value):
138 self.config[fname][key] = value
139
140 def __hash__(self):
141 val = 0
142 for fname in self.config:
Simon Glassc78ed662019-10-31 07:42:53 -0600143 for key, value in self.config[fname].items():
144 print(key, value)
Simon Glasscad8abf2015-08-25 21:52:14 -0600145 val = val ^ hash(key) & hash(value)
146 return val
Simon Glassc05694f2013-04-03 11:07:16 +0000147
Alex Kiernan4059e302018-05-31 04:48:34 +0000148class Environment:
149 """Holds information about environment variables for a board."""
150 def __init__(self, target):
151 self.target = target
152 self.environment = {}
153
154 def Add(self, key, value):
155 self.environment[key] = value
156
Simon Glassc05694f2013-04-03 11:07:16 +0000157class Builder:
158 """Class for building U-Boot for a particular commit.
159
160 Public members: (many should ->private)
Simon Glassc05694f2013-04-03 11:07:16 +0000161 already_done: Number of builds already completed
162 base_dir: Base directory to use for builder
163 checkout: True to check out source, False to skip that step.
164 This is used for testing.
165 col: terminal.Color() object
166 count: Number of commits to build
167 do_make: Method to call to invoke Make
168 fail: Number of builds that failed due to error
169 force_build: Force building even if a build already exists
170 force_config_on_failure: If a commit fails for a board, disable
171 incremental building for the next commit we build for that
172 board, so that we will see all warnings/errors again.
Simon Glass7041c392014-07-13 12:22:31 -0600173 force_build_failures: If a previously-built build (i.e. built on
174 a previous run of buildman) is marked as failed, rebuild it.
Simon Glassc05694f2013-04-03 11:07:16 +0000175 git_dir: Git directory containing source repository
Simon Glassc05694f2013-04-03 11:07:16 +0000176 num_jobs: Number of jobs to run at once (passed to make as -j)
177 num_threads: Number of builder threads to run
178 out_queue: Queue of results to process
179 re_make_err: Compiled regular expression for ignore_lines
180 queue: Queue of jobs to run
181 threads: List of active threads
182 toolchains: Toolchains object to use for building
183 upto: Current commit number we are building (0.count-1)
184 warned: Number of builds that produced at least one warning
Simon Glassf3018b7a2014-07-14 17:51:02 -0600185 force_reconfig: Reconfigure U-Boot on each comiit. This disables
186 incremental building, where buildman reconfigures on the first
187 commit for a baord, and then just does an incremental build for
188 the following commits. In fact buildman will reconfigure and
189 retry for any failing commits, so generally the only effect of
190 this option is to slow things down.
Simon Glass38df2e22014-07-14 17:51:03 -0600191 in_tree: Build U-Boot in-tree instead of specifying an output
192 directory separate from the source code. This option is really
193 only useful for testing in-tree builds.
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600194 work_in_output: Use the output directory as the work directory and
195 don't write to a separate output directory.
Simon Glass9bf9a722021-04-11 16:27:27 +1200196 thread_exceptions: List of exceptions raised by thread jobs
Simon Glassc05694f2013-04-03 11:07:16 +0000197
198 Private members:
199 _base_board_dict: Last-summarised Dict of boards
200 _base_err_lines: Last-summarised list of errors
Simon Glass03749d42014-08-28 09:43:44 -0600201 _base_warn_lines: Last-summarised list of warnings
Simon Glassc05694f2013-04-03 11:07:16 +0000202 _build_period_us: Time taken for a single build (float object).
203 _complete_delay: Expected delay until completion (timedelta)
204 _next_delay_update: Next time we plan to display a progress update
205 (datatime)
206 _show_unknown: Show unknown boards (those not built) in summary
Simon Glass726ae812020-04-09 15:08:47 -0600207 _start_time: Start time for the build
Simon Glassc05694f2013-04-03 11:07:16 +0000208 _timestamps: List of timestamps for the completion of the last
209 last _timestamp_count builds. Each is a datetime object.
210 _timestamp_count: Number of timestamps to keep in our list.
211 _working_dir: Base working directory containing all threads
Simon Glassc635d892021-01-30 22:17:46 -0700212 _single_builder: BuilderThread object for the singer builder, if
213 threading is not being used
Simon Glass146b6022021-10-19 21:43:24 -0600214 _terminated: Thread was terminated due to an error
215 _restarting_config: True if 'Restart config' is detected in output
Simon Glass6c435622022-07-11 19:03:56 -0600216 _ide: Produce output suitable for an Integrated Development Environment,
217 i.e. dont emit progress information and put errors/warnings on stderr
Simon Glassc05694f2013-04-03 11:07:16 +0000218 """
219 class Outcome:
220 """Records a build outcome for a single make invocation
221
222 Public Members:
223 rc: Outcome value (OUTCOME_...)
224 err_lines: List of error lines or [] if none
225 sizes: Dictionary of image size information, keyed by filename
226 - Each value is itself a dictionary containing
227 values for 'text', 'data' and 'bss', being the integer
228 size in bytes of each section.
229 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
230 value is itself a dictionary:
231 key: function name
232 value: Size of function in bytes
Simon Glassdb17fb82015-02-05 22:06:15 -0700233 config: Dictionary keyed by filename - e.g. '.config'. Each
234 value is itself a dictionary:
235 key: config name
236 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +0000237 environment: Dictionary keyed by environment variable, Each
238 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +0000239 """
Alex Kiernan4059e302018-05-31 04:48:34 +0000240 def __init__(self, rc, err_lines, sizes, func_sizes, config,
241 environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000242 self.rc = rc
243 self.err_lines = err_lines
244 self.sizes = sizes
245 self.func_sizes = func_sizes
Simon Glassdb17fb82015-02-05 22:06:15 -0700246 self.config = config
Alex Kiernan4059e302018-05-31 04:48:34 +0000247 self.environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +0000248
249 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glasse87bde12014-12-01 17:33:55 -0700250 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warren97c96902016-04-11 10:48:44 -0600251 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glass6029af12020-04-09 15:08:51 -0600252 mrproper=False, per_board_out_dir=False,
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100253 config_only=False, squash_config_y=False,
Simon Glass9bf9a722021-04-11 16:27:27 +1200254 warnings_as_errors=False, work_in_output=False,
Simon Glasse5650a82022-01-22 05:07:33 -0700255 test_thread_exceptions=False, adjust_cfg=None):
Simon Glassc05694f2013-04-03 11:07:16 +0000256 """Create a new Builder object
257
258 Args:
259 toolchains: Toolchains object to use for building
260 base_dir: Base directory to use for builder
261 git_dir: Git directory containing source repository
262 num_threads: Number of builder threads to run
263 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900264 gnu_make: the command name of GNU Make.
Simon Glassc05694f2013-04-03 11:07:16 +0000265 checkout: True to check out source, False to skip that step.
266 This is used for testing.
267 show_unknown: Show unknown boards (those not built) in summary
268 step: 1 to process every commit, n to process every nth commit
Simon Glassd48a46c2014-12-01 17:34:00 -0700269 no_subdirs: Don't create subdirectories when building current
270 source for a single board
271 full_path: Return the full path in CROSS_COMPILE and don't set
272 PATH
Simon Glass655b6102014-12-01 17:34:07 -0700273 verbose_build: Run build with V=1 and don't use 'make -s'
Simon Glass6029af12020-04-09 15:08:51 -0600274 mrproper: Always run 'make mrproper' when configuring
Stephen Warren97c96902016-04-11 10:48:44 -0600275 per_board_out_dir: Build in a separate persistent directory per
276 board rather than a thread-specific directory
Simon Glass739e8512016-11-13 14:25:51 -0700277 config_only: Only configure each build, don't build it
Simon Glasscde5c302016-11-13 14:25:53 -0700278 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100279 warnings_as_errors: Treat all compiler warnings as errors
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600280 work_in_output: Use the output directory as the work directory and
281 don't write to a separate output directory.
Simon Glass9bf9a722021-04-11 16:27:27 +1200282 test_thread_exceptions: Uses for tests only, True to make the
283 threads raise an exception instead of reporting their result.
284 This simulates a failure in the code somewhere
Simon Glasse5650a82022-01-22 05:07:33 -0700285 adjust_cfg_list (list of str): List of changes to make to .config
286 file before building. Each is one of (where C is the config
287 option with or without the CONFIG_ prefix)
288
289 C to enable C
290 ~C to disable C
291 C=val to set the value of C (val must have quotes if C is
292 a string Kconfig
293
Simon Glassc05694f2013-04-03 11:07:16 +0000294 """
295 self.toolchains = toolchains
296 self.base_dir = base_dir
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600297 if work_in_output:
298 self._working_dir = base_dir
299 else:
300 self._working_dir = os.path.join(base_dir, '.bm-work')
Simon Glassc05694f2013-04-03 11:07:16 +0000301 self.threads = []
Simon Glassc05694f2013-04-03 11:07:16 +0000302 self.do_make = self.Make
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900303 self.gnu_make = gnu_make
Simon Glassc05694f2013-04-03 11:07:16 +0000304 self.checkout = checkout
305 self.num_threads = num_threads
306 self.num_jobs = num_jobs
307 self.already_done = 0
308 self.force_build = False
309 self.git_dir = git_dir
310 self._show_unknown = show_unknown
311 self._timestamp_count = 10
312 self._build_period_us = None
313 self._complete_delay = None
314 self._next_delay_update = datetime.now()
Simon Glass726ae812020-04-09 15:08:47 -0600315 self._start_time = datetime.now()
Simon Glassc05694f2013-04-03 11:07:16 +0000316 self.force_config_on_failure = True
Simon Glass7041c392014-07-13 12:22:31 -0600317 self.force_build_failures = False
Simon Glassf3018b7a2014-07-14 17:51:02 -0600318 self.force_reconfig = False
Simon Glassc05694f2013-04-03 11:07:16 +0000319 self._step = step
Simon Glass38df2e22014-07-14 17:51:03 -0600320 self.in_tree = False
Simon Glassbb4dffb2014-08-09 15:33:06 -0600321 self._error_lines = 0
Simon Glasse87bde12014-12-01 17:33:55 -0700322 self.no_subdirs = no_subdirs
Simon Glassd48a46c2014-12-01 17:34:00 -0700323 self.full_path = full_path
Simon Glass655b6102014-12-01 17:34:07 -0700324 self.verbose_build = verbose_build
Simon Glass739e8512016-11-13 14:25:51 -0700325 self.config_only = config_only
Simon Glasscde5c302016-11-13 14:25:53 -0700326 self.squash_config_y = squash_config_y
327 self.config_filenames = BASE_CONFIG_FILENAMES
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600328 self.work_in_output = work_in_output
Simon Glasse5650a82022-01-22 05:07:33 -0700329 self.adjust_cfg = adjust_cfg
Simon Glass6c435622022-07-11 19:03:56 -0600330 self._ide = False
Simon Glasse5650a82022-01-22 05:07:33 -0700331
Simon Glasscde5c302016-11-13 14:25:53 -0700332 if not self.squash_config_y:
333 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glass146b6022021-10-19 21:43:24 -0600334 self._terminated = False
335 self._restarting_config = False
Simon Glassc05694f2013-04-03 11:07:16 +0000336
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100337 self.warnings_as_errors = warnings_as_errors
Simon Glassc05694f2013-04-03 11:07:16 +0000338 self.col = terminal.Color()
339
Simon Glass03749d42014-08-28 09:43:44 -0600340 self._re_function = re.compile('(.*): In function.*')
341 self._re_files = re.compile('In file included from.*')
342 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
Simon Glass0db94432018-11-06 16:02:11 -0700343 self._re_dtb_warning = re.compile('(.*): Warning .*')
Simon Glass03749d42014-08-28 09:43:44 -0600344 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
Simon Glassf4ebfba2020-04-09 15:08:53 -0600345 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
346 re.MULTILINE | re.DOTALL)
Simon Glass03749d42014-08-28 09:43:44 -0600347
Simon Glass9bf9a722021-04-11 16:27:27 +1200348 self.thread_exceptions = []
349 self.test_thread_exceptions = test_thread_exceptions
Simon Glassc635d892021-01-30 22:17:46 -0700350 if self.num_threads:
351 self._single_builder = None
352 self.queue = queue.Queue()
353 self.out_queue = queue.Queue()
354 for i in range(self.num_threads):
Simon Glass9bf9a722021-04-11 16:27:27 +1200355 t = builderthread.BuilderThread(
356 self, i, mrproper, per_board_out_dir,
357 test_exception=test_thread_exceptions)
Simon Glassc635d892021-01-30 22:17:46 -0700358 t.setDaemon(True)
359 t.start()
360 self.threads.append(t)
361
362 t = builderthread.ResultThread(self)
Simon Glassc05694f2013-04-03 11:07:16 +0000363 t.setDaemon(True)
364 t.start()
365 self.threads.append(t)
Simon Glassc635d892021-01-30 22:17:46 -0700366 else:
367 self._single_builder = builderthread.BuilderThread(
368 self, -1, mrproper, per_board_out_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000369
370 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
371 self.re_make_err = re.compile('|'.join(ignore_lines))
372
Simon Glass205ac042016-09-18 16:48:37 -0600373 # Handle existing graceful with SIGINT / Ctrl-C
374 signal.signal(signal.SIGINT, self.signal_handler)
375
Simon Glassc05694f2013-04-03 11:07:16 +0000376 def __del__(self):
377 """Get rid of all threads created by the builder"""
378 for t in self.threads:
379 del t
380
Simon Glass205ac042016-09-18 16:48:37 -0600381 def signal_handler(self, signal, frame):
382 sys.exit(1)
383
Simon Glasseb48bbc2014-08-09 15:33:02 -0600384 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glass3394c9f2014-08-28 09:43:43 -0600385 show_detail=False, show_bloat=False,
Alex Kiernan4059e302018-05-31 04:48:34 +0000386 list_error_boards=False, show_config=False,
Simon Glassf4ebfba2020-04-09 15:08:53 -0600387 show_environment=False, filter_dtb_warnings=False,
Simon Glass6c435622022-07-11 19:03:56 -0600388 filter_migration_warnings=False, ide=False):
Simon Glasseb48bbc2014-08-09 15:33:02 -0600389 """Setup display options for the builder.
390
Simon Glass9ea93812020-04-09 15:08:52 -0600391 Args:
392 show_errors: True to show summarised error/warning info
393 show_sizes: Show size deltas
394 show_detail: Show size delta detail for each board if show_sizes
395 show_bloat: Show detail for each function
396 list_error_boards: Show the boards which caused each error/warning
397 show_config: Show config deltas
398 show_environment: Show environment deltas
399 filter_dtb_warnings: Filter out any warnings from the device-tree
400 compiler
Simon Glassf4ebfba2020-04-09 15:08:53 -0600401 filter_migration_warnings: Filter out any warnings about migrating
402 a board to driver model
Simon Glass6c435622022-07-11 19:03:56 -0600403 ide: Create output that can be parsed by an IDE. There is no '+' prefix on
404 error lines and output on stderr stays on stderr.
Simon Glasseb48bbc2014-08-09 15:33:02 -0600405 """
406 self._show_errors = show_errors
407 self._show_sizes = show_sizes
408 self._show_detail = show_detail
409 self._show_bloat = show_bloat
Simon Glass3394c9f2014-08-28 09:43:43 -0600410 self._list_error_boards = list_error_boards
Simon Glassdb17fb82015-02-05 22:06:15 -0700411 self._show_config = show_config
Alex Kiernan4059e302018-05-31 04:48:34 +0000412 self._show_environment = show_environment
Simon Glass9ea93812020-04-09 15:08:52 -0600413 self._filter_dtb_warnings = filter_dtb_warnings
Simon Glassf4ebfba2020-04-09 15:08:53 -0600414 self._filter_migration_warnings = filter_migration_warnings
Simon Glass6c435622022-07-11 19:03:56 -0600415 self._ide = ide
Simon Glasseb48bbc2014-08-09 15:33:02 -0600416
Simon Glassc05694f2013-04-03 11:07:16 +0000417 def _AddTimestamp(self):
418 """Add a new timestamp to the list and record the build period.
419
420 The build period is the length of time taken to perform a single
421 build (one board, one commit).
422 """
423 now = datetime.now()
424 self._timestamps.append(now)
425 count = len(self._timestamps)
426 delta = self._timestamps[-1] - self._timestamps[0]
427 seconds = delta.total_seconds()
428
429 # If we have enough data, estimate build period (time taken for a
430 # single build) and therefore completion time.
431 if count > 1 and self._next_delay_update < now:
432 self._next_delay_update = now + timedelta(seconds=2)
433 if seconds > 0:
434 self._build_period = float(seconds) / count
435 todo = self.count - self.upto
436 self._complete_delay = timedelta(microseconds=
437 self._build_period * todo * 1000000)
438 # Round it
439 self._complete_delay -= timedelta(
440 microseconds=self._complete_delay.microseconds)
441
442 if seconds > 60:
443 self._timestamps.popleft()
444 count -= 1
445
Simon Glassc05694f2013-04-03 11:07:16 +0000446 def SelectCommit(self, commit, checkout=True):
447 """Checkout the selected commit for this build
448 """
449 self.commit = commit
450 if checkout and self.checkout:
Simon Glass761648b2022-01-29 14:14:11 -0700451 gitutil.checkout(commit.hash)
Simon Glassc05694f2013-04-03 11:07:16 +0000452
453 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
454 """Run make
455
456 Args:
457 commit: Commit object that is being built
458 brd: Board object that is being built
Roger Meiere0a0e552014-08-20 22:10:29 +0200459 stage: Stage that we are at (mrproper, config, build)
Simon Glassc05694f2013-04-03 11:07:16 +0000460 cwd: Directory where make should be run
461 args: Arguments to pass to make
Simon Glass840be732022-01-29 14:14:05 -0700462 kwargs: Arguments to pass to command.run_pipe()
Simon Glassc05694f2013-04-03 11:07:16 +0000463 """
Simon Glass146b6022021-10-19 21:43:24 -0600464
465 def check_output(stream, data):
466 if b'Restart config' in data:
467 self._restarting_config = True
468
469 # If we see 'Restart config' following by multiple errors
470 if self._restarting_config:
471 m = RE_NO_DEFAULT.findall(data)
472
473 # Number of occurences of each Kconfig item
474 multiple = [m.count(val) for val in set(m)]
475
476 # If any of them occur more than once, we have a loop
477 if [val for val in multiple if val > 1]:
478 self._terminated = True
479 return True
480 return False
481
482 self._restarting_config = False
483 self._terminated = False
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900484 cmd = [self.gnu_make] + list(args)
Simon Glass840be732022-01-29 14:14:05 -0700485 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
Simon Glass146b6022021-10-19 21:43:24 -0600486 cwd=cwd, raise_on_error=False, infile='/dev/null',
487 output_func=check_output, **kwargs)
488
489 if self._terminated:
490 # Try to be helpful
491 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
492
Simon Glass413f91a2015-02-05 22:06:12 -0700493 if self.verbose_build:
494 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
495 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassc05694f2013-04-03 11:07:16 +0000496 return result
497
498 def ProcessResult(self, result):
499 """Process the result of a build, showing progress information
500
501 Args:
Simon Glass78e418e2014-08-09 15:33:03 -0600502 result: A CommandResult object, which indicates the result for
503 a single build
Simon Glassc05694f2013-04-03 11:07:16 +0000504 """
505 col = terminal.Color()
506 if result:
507 target = result.brd.target
508
Simon Glassc05694f2013-04-03 11:07:16 +0000509 self.upto += 1
510 if result.return_code != 0:
511 self.fail += 1
512 elif result.stderr:
513 self.warned += 1
514 if result.already_done:
515 self.already_done += 1
Simon Glass78e418e2014-08-09 15:33:03 -0600516 if self._verbose:
Simon Glass02811582022-01-29 14:14:18 -0700517 terminal.print_clear()
Simon Glass78e418e2014-08-09 15:33:03 -0600518 boards_selected = {target : result.brd}
519 self.ResetResultSummary(boards_selected)
520 self.ProduceResultSummary(result.commit_upto, self.commits,
521 boards_selected)
Simon Glassc05694f2013-04-03 11:07:16 +0000522 else:
523 target = '(starting)'
524
525 # Display separate counts for ok, warned and fail
526 ok = self.upto - self.warned - self.fail
Simon Glassf45d3742022-01-29 14:14:17 -0700527 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
528 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
529 line += self.col.build(self.col.RED, '%5d' % self.fail)
Simon Glassc05694f2013-04-03 11:07:16 +0000530
Simon Glass69c3a8a2020-04-09 15:08:45 -0600531 line += ' /%-5d ' % self.count
532 remaining = self.count - self.upto
533 if remaining:
Simon Glassf45d3742022-01-29 14:14:17 -0700534 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
Simon Glass69c3a8a2020-04-09 15:08:45 -0600535 else:
536 line += ' ' * 8
Simon Glassc05694f2013-04-03 11:07:16 +0000537
538 # Add our current completion time estimate
539 self._AddTimestamp()
540 if self._complete_delay:
Simon Glass69c3a8a2020-04-09 15:08:45 -0600541 line += '%s : ' % self._complete_delay
Simon Glassc05694f2013-04-03 11:07:16 +0000542
Simon Glass69c3a8a2020-04-09 15:08:45 -0600543 line += target
Simon Glass6c435622022-07-11 19:03:56 -0600544 if not self._ide:
545 terminal.print_clear()
546 tprint(line, newline=False, limit_to_line=True)
Simon Glassc05694f2013-04-03 11:07:16 +0000547
548 def _GetOutputDir(self, commit_upto):
549 """Get the name of the output directory for a commit number
550
551 The output directory is typically .../<branch>/<commit>.
552
553 Args:
554 commit_upto: Commit number to use (0..self.count-1)
555 """
Simon Glasse3c85ab2020-04-17 17:51:34 -0600556 if self.work_in_output:
557 return self._working_dir
558
Simon Glasse87bde12014-12-01 17:33:55 -0700559 commit_dir = None
Simon Glassd326ad72014-08-09 15:32:59 -0600560 if self.commits:
561 commit = self.commits[commit_upto]
562 subject = commit.subject.translate(trans_valid_chars)
Simon Glass5dc1ca72020-03-18 09:42:45 -0600563 # See _GetOutputSpaceRemovals() which parses this name
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +0300564 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
565 commit.hash, subject[:20]))
Simon Glasse87bde12014-12-01 17:33:55 -0700566 elif not self.no_subdirs:
Simon Glassd326ad72014-08-09 15:32:59 -0600567 commit_dir = 'current'
Simon Glasse87bde12014-12-01 17:33:55 -0700568 if not commit_dir:
569 return self.base_dir
570 return os.path.join(self.base_dir, commit_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000571
572 def GetBuildDir(self, commit_upto, target):
573 """Get the name of the build directory for a commit number
574
575 The build directory is typically .../<branch>/<commit>/<target>.
576
577 Args:
578 commit_upto: Commit number to use (0..self.count-1)
579 target: Target name
580 """
581 output_dir = self._GetOutputDir(commit_upto)
Simon Glasse3c85ab2020-04-17 17:51:34 -0600582 if self.work_in_output:
583 return output_dir
Simon Glassc05694f2013-04-03 11:07:16 +0000584 return os.path.join(output_dir, target)
585
586 def GetDoneFile(self, commit_upto, target):
587 """Get the name of the done file for a commit number
588
589 Args:
590 commit_upto: Commit number to use (0..self.count-1)
591 target: Target name
592 """
593 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
594
595 def GetSizesFile(self, commit_upto, target):
596 """Get the name of the sizes file for a commit number
597
598 Args:
599 commit_upto: Commit number to use (0..self.count-1)
600 target: Target name
601 """
602 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
603
604 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
605 """Get the name of the funcsizes file for a commit number and ELF file
606
607 Args:
608 commit_upto: Commit number to use (0..self.count-1)
609 target: Target name
610 elf_fname: Filename of elf image
611 """
612 return os.path.join(self.GetBuildDir(commit_upto, target),
613 '%s.sizes' % elf_fname.replace('/', '-'))
614
615 def GetObjdumpFile(self, commit_upto, target, elf_fname):
616 """Get the name of the objdump file for a commit number and ELF file
617
618 Args:
619 commit_upto: Commit number to use (0..self.count-1)
620 target: Target name
621 elf_fname: Filename of elf image
622 """
623 return os.path.join(self.GetBuildDir(commit_upto, target),
624 '%s.objdump' % elf_fname.replace('/', '-'))
625
626 def GetErrFile(self, commit_upto, target):
627 """Get the name of the err file for a commit number
628
629 Args:
630 commit_upto: Commit number to use (0..self.count-1)
631 target: Target name
632 """
633 output_dir = self.GetBuildDir(commit_upto, target)
634 return os.path.join(output_dir, 'err')
635
636 def FilterErrors(self, lines):
637 """Filter out errors in which we have no interest
638
639 We should probably use map().
640
641 Args:
642 lines: List of error lines, each a string
643 Returns:
644 New list with only interesting lines included
645 """
646 out_lines = []
Simon Glassf4ebfba2020-04-09 15:08:53 -0600647 if self._filter_migration_warnings:
648 text = '\n'.join(lines)
649 text = self._re_migration_warning.sub('', text)
650 lines = text.splitlines()
Simon Glassc05694f2013-04-03 11:07:16 +0000651 for line in lines:
Simon Glass9ea93812020-04-09 15:08:52 -0600652 if self.re_make_err.search(line):
653 continue
654 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
655 continue
656 out_lines.append(line)
Simon Glassc05694f2013-04-03 11:07:16 +0000657 return out_lines
658
659 def ReadFuncSizes(self, fname, fd):
660 """Read function sizes from the output of 'nm'
661
662 Args:
663 fd: File containing data to read
664 fname: Filename we are reading from (just for errors)
665
666 Returns:
667 Dictionary containing size of each function in bytes, indexed by
668 function name.
669 """
670 sym = {}
671 for line in fd.readlines():
672 try:
Tom Rini08d34fc2019-12-06 15:31:31 -0500673 if line.strip():
674 size, type, name = line[:-1].split()
Simon Glassc05694f2013-04-03 11:07:16 +0000675 except:
Simon Glass02811582022-01-29 14:14:18 -0700676 tprint("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassc05694f2013-04-03 11:07:16 +0000677 continue
678 if type in 'tTdDbB':
679 # function names begin with '.' on 64-bit powerpc
680 if '.' in name[1:]:
681 name = 'static.' + name.split('.')[0]
682 sym[name] = sym.get(name, 0) + int(size, 16)
683 return sym
684
Simon Glassdb17fb82015-02-05 22:06:15 -0700685 def _ProcessConfig(self, fname):
686 """Read in a .config, autoconf.mk or autoconf.h file
687
688 This function handles all config file types. It ignores comments and
689 any #defines which don't start with CONFIG_.
690
691 Args:
692 fname: Filename to read
693
694 Returns:
695 Dictionary:
696 key: Config name (e.g. CONFIG_DM)
697 value: Config value (e.g. 1)
698 """
699 config = {}
700 if os.path.exists(fname):
701 with open(fname) as fd:
702 for line in fd:
703 line = line.strip()
704 if line.startswith('#define'):
705 values = line[8:].split(' ', 1)
706 if len(values) > 1:
707 key, value = values
708 else:
709 key = values[0]
Simon Glasscde5c302016-11-13 14:25:53 -0700710 value = '1' if self.squash_config_y else ''
Simon Glassdb17fb82015-02-05 22:06:15 -0700711 if not key.startswith('CONFIG_'):
712 continue
713 elif not line or line[0] in ['#', '*', '/']:
714 continue
715 else:
716 key, value = line.split('=', 1)
Simon Glasscde5c302016-11-13 14:25:53 -0700717 if self.squash_config_y and value == 'y':
718 value = '1'
Simon Glassdb17fb82015-02-05 22:06:15 -0700719 config[key] = value
720 return config
721
Alex Kiernan4059e302018-05-31 04:48:34 +0000722 def _ProcessEnvironment(self, fname):
723 """Read in a uboot.env file
724
725 This function reads in environment variables from a file.
726
727 Args:
728 fname: Filename to read
729
730 Returns:
731 Dictionary:
732 key: environment variable (e.g. bootlimit)
733 value: value of environment variable (e.g. 1)
734 """
735 environment = {}
736 if os.path.exists(fname):
737 with open(fname) as fd:
738 for line in fd.read().split('\0'):
739 try:
740 key, value = line.split('=', 1)
741 environment[key] = value
742 except ValueError:
743 # ignore lines we can't parse
744 pass
745 return environment
746
Simon Glassdb17fb82015-02-05 22:06:15 -0700747 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000748 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000749 """Work out the outcome of a build.
750
751 Args:
752 commit_upto: Commit number to check (0..n-1)
753 target: Target board to check
754 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700755 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000756 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000757
758 Returns:
759 Outcome object
760 """
761 done_file = self.GetDoneFile(commit_upto, target)
762 sizes_file = self.GetSizesFile(commit_upto, target)
763 sizes = {}
764 func_sizes = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700765 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000766 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000767 if os.path.exists(done_file):
768 with open(done_file, 'r') as fd:
Simon Glass91c54a42019-04-26 19:02:23 -0600769 try:
770 return_code = int(fd.readline())
771 except ValueError:
772 # The file may be empty due to running out of disk space.
773 # Try a rebuild
774 return_code = 1
Simon Glassc05694f2013-04-03 11:07:16 +0000775 err_lines = []
776 err_file = self.GetErrFile(commit_upto, target)
777 if os.path.exists(err_file):
778 with open(err_file, 'r') as fd:
779 err_lines = self.FilterErrors(fd.readlines())
780
781 # Decide whether the build was ok, failed or created warnings
782 if return_code:
783 rc = OUTCOME_ERROR
784 elif len(err_lines):
785 rc = OUTCOME_WARNING
786 else:
787 rc = OUTCOME_OK
788
789 # Convert size information to our simple format
790 if os.path.exists(sizes_file):
791 with open(sizes_file, 'r') as fd:
792 for line in fd.readlines():
793 values = line.split()
794 rodata = 0
795 if len(values) > 6:
796 rodata = int(values[6], 16)
797 size_dict = {
798 'all' : int(values[0]) + int(values[1]) +
799 int(values[2]),
800 'text' : int(values[0]) - rodata,
801 'data' : int(values[1]),
802 'bss' : int(values[2]),
803 'rodata' : rodata,
804 }
805 sizes[values[5]] = size_dict
806
807 if read_func_sizes:
808 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
809 for fname in glob.glob(pattern):
810 with open(fname, 'r') as fd:
811 dict_name = os.path.basename(fname).replace('.sizes',
812 '')
813 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
814
Simon Glassdb17fb82015-02-05 22:06:15 -0700815 if read_config:
816 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glasscde5c302016-11-13 14:25:53 -0700817 for name in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700818 fname = os.path.join(output_dir, name)
819 config[name] = self._ProcessConfig(fname)
820
Alex Kiernan4059e302018-05-31 04:48:34 +0000821 if read_environment:
822 output_dir = self.GetBuildDir(commit_upto, target)
823 fname = os.path.join(output_dir, 'uboot.env')
824 environment = self._ProcessEnvironment(fname)
825
826 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
827 environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000828
Alex Kiernan4059e302018-05-31 04:48:34 +0000829 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000830
Simon Glassdb17fb82015-02-05 22:06:15 -0700831 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000832 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000833 """Calculate a summary of the results of building a commit.
834
835 Args:
836 board_selected: Dict containing boards to summarise
837 commit_upto: Commit number to summarize (0..self.count-1)
838 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700839 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000840 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000841
842 Returns:
843 Tuple:
Simon Glass6c435622022-07-11 19:03:56 -0600844 Dict containing boards which built this commit:
845 key: board.target
846 value: Builder.Outcome object
Simon Glass03749d42014-08-28 09:43:44 -0600847 List containing a summary of error lines
Simon Glass3394c9f2014-08-28 09:43:43 -0600848 Dict keyed by error line, containing a list of the Board
849 objects with that error
Simon Glass03749d42014-08-28 09:43:44 -0600850 List containing a summary of warning lines
851 Dict keyed by error line, containing a list of the Board
852 objects with that warning
Simon Glasscad8abf2015-08-25 21:52:14 -0600853 Dictionary keyed by board.target. Each value is a dictionary:
854 key: filename - e.g. '.config'
Simon Glassdb17fb82015-02-05 22:06:15 -0700855 value is itself a dictionary:
856 key: config name
857 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +0000858 Dictionary keyed by board.target. Each value is a dictionary:
859 key: environment variable
860 value: value of environment variable
Simon Glassc05694f2013-04-03 11:07:16 +0000861 """
Simon Glass03749d42014-08-28 09:43:44 -0600862 def AddLine(lines_summary, lines_boards, line, board):
863 line = line.rstrip()
864 if line in lines_boards:
865 lines_boards[line].append(board)
866 else:
867 lines_boards[line] = [board]
868 lines_summary.append(line)
869
Simon Glassc05694f2013-04-03 11:07:16 +0000870 board_dict = {}
871 err_lines_summary = []
Simon Glass3394c9f2014-08-28 09:43:43 -0600872 err_lines_boards = {}
Simon Glass03749d42014-08-28 09:43:44 -0600873 warn_lines_summary = []
874 warn_lines_boards = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700875 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000876 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000877
Simon Glass8132f982022-07-11 19:03:57 -0600878 for brd in boards_selected.values():
879 outcome = self.GetBuildOutcome(commit_upto, brd.target,
Alex Kiernan4059e302018-05-31 04:48:34 +0000880 read_func_sizes, read_config,
881 read_environment)
Simon Glass8132f982022-07-11 19:03:57 -0600882 board_dict[brd.target] = outcome
Simon Glass03749d42014-08-28 09:43:44 -0600883 last_func = None
884 last_was_warning = False
885 for line in outcome.err_lines:
886 if line:
887 if (self._re_function.match(line) or
888 self._re_files.match(line)):
889 last_func = line
Simon Glass3394c9f2014-08-28 09:43:43 -0600890 else:
Simon Glass0db94432018-11-06 16:02:11 -0700891 is_warning = (self._re_warning.match(line) or
892 self._re_dtb_warning.match(line))
Simon Glass03749d42014-08-28 09:43:44 -0600893 is_note = self._re_note.match(line)
894 if is_warning or (last_was_warning and is_note):
895 if last_func:
896 AddLine(warn_lines_summary, warn_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600897 last_func, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600898 AddLine(warn_lines_summary, warn_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600899 line, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600900 else:
901 if last_func:
902 AddLine(err_lines_summary, err_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600903 last_func, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600904 AddLine(err_lines_summary, err_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600905 line, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600906 last_was_warning = is_warning
907 last_func = None
Simon Glass8132f982022-07-11 19:03:57 -0600908 tconfig = Config(self.config_filenames, brd.target)
Simon Glasscde5c302016-11-13 14:25:53 -0700909 for fname in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700910 if outcome.config:
Simon Glassc78ed662019-10-31 07:42:53 -0600911 for key, value in outcome.config[fname].items():
Simon Glasscad8abf2015-08-25 21:52:14 -0600912 tconfig.Add(fname, key, value)
Simon Glass8132f982022-07-11 19:03:57 -0600913 config[brd.target] = tconfig
Simon Glassdb17fb82015-02-05 22:06:15 -0700914
Simon Glass8132f982022-07-11 19:03:57 -0600915 tenvironment = Environment(brd.target)
Alex Kiernan4059e302018-05-31 04:48:34 +0000916 if outcome.environment:
Simon Glassc78ed662019-10-31 07:42:53 -0600917 for key, value in outcome.environment.items():
Alex Kiernan4059e302018-05-31 04:48:34 +0000918 tenvironment.Add(key, value)
Simon Glass8132f982022-07-11 19:03:57 -0600919 environment[brd.target] = tenvironment
Alex Kiernan4059e302018-05-31 04:48:34 +0000920
Simon Glass03749d42014-08-28 09:43:44 -0600921 return (board_dict, err_lines_summary, err_lines_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +0000922 warn_lines_summary, warn_lines_boards, config, environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000923
924 def AddOutcome(self, board_dict, arch_list, changes, char, color):
925 """Add an output to our list of outcomes for each architecture
926
927 This simple function adds failing boards (changes) to the
928 relevant architecture string, so we can print the results out
929 sorted by architecture.
930
931 Args:
932 board_dict: Dict containing all boards
933 arch_list: Dict keyed by arch name. Value is a string containing
934 a list of board names which failed for that arch.
935 changes: List of boards to add to arch_list
936 color: terminal.Colour object
937 """
938 done_arch = {}
939 for target in changes:
940 if target in board_dict:
941 arch = board_dict[target].arch
942 else:
943 arch = 'unknown'
Simon Glassf45d3742022-01-29 14:14:17 -0700944 str = self.col.build(color, ' ' + target)
Simon Glassc05694f2013-04-03 11:07:16 +0000945 if not arch in done_arch:
Simon Glassf45d3742022-01-29 14:14:17 -0700946 str = ' %s %s' % (self.col.build(color, char), str)
Simon Glassc05694f2013-04-03 11:07:16 +0000947 done_arch[arch] = True
948 if not arch in arch_list:
949 arch_list[arch] = str
950 else:
951 arch_list[arch] += str
952
953
954 def ColourNum(self, num):
955 color = self.col.RED if num > 0 else self.col.GREEN
956 if num == 0:
957 return '0'
Simon Glassf45d3742022-01-29 14:14:17 -0700958 return self.col.build(color, str(num))
Simon Glassc05694f2013-04-03 11:07:16 +0000959
960 def ResetResultSummary(self, board_selected):
961 """Reset the results summary ready for use.
962
963 Set up the base board list to be all those selected, and set the
964 error lines to empty.
965
966 Following this, calls to PrintResultSummary() will use this
967 information to work out what has changed.
968
969 Args:
970 board_selected: Dict containing boards to summarise, keyed by
971 board.target
972 """
973 self._base_board_dict = {}
Simon Glass8132f982022-07-11 19:03:57 -0600974 for brd in board_selected:
975 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000976 self._base_err_lines = []
Simon Glass03749d42014-08-28 09:43:44 -0600977 self._base_warn_lines = []
978 self._base_err_line_boards = {}
979 self._base_warn_line_boards = {}
Simon Glasscad8abf2015-08-25 21:52:14 -0600980 self._base_config = None
Alex Kiernan4059e302018-05-31 04:48:34 +0000981 self._base_environment = None
Simon Glassc05694f2013-04-03 11:07:16 +0000982
983 def PrintFuncSizeDetail(self, fname, old, new):
984 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
985 delta, common = [], {}
986
987 for a in old:
988 if a in new:
989 common[a] = 1
990
991 for name in old:
992 if name not in common:
993 remove += 1
994 down += old[name]
995 delta.append([-old[name], name])
996
997 for name in new:
998 if name not in common:
999 add += 1
1000 up += new[name]
1001 delta.append([new[name], name])
1002
1003 for name in common:
1004 diff = new.get(name, 0) - old.get(name, 0)
1005 if diff > 0:
1006 grow, up = grow + 1, up + diff
1007 elif diff < 0:
1008 shrink, down = shrink + 1, down - diff
1009 delta.append([diff, name])
1010
1011 delta.sort()
1012 delta.reverse()
1013
1014 args = [add, -remove, grow, -shrink, up, -down, up - down]
Tom Rini0b48cd62017-05-22 13:48:52 -04001015 if max(args) == 0 and min(args) == 0:
Simon Glassc05694f2013-04-03 11:07:16 +00001016 return
1017 args = [self.ColourNum(x) for x in args]
1018 indent = ' ' * 15
Simon Glass02811582022-01-29 14:14:18 -07001019 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
Simon Glassf45d3742022-01-29 14:14:17 -07001020 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
Simon Glass02811582022-01-29 14:14:18 -07001021 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
Simon Glass4433aa92014-09-05 19:00:07 -06001022 'delta'))
Simon Glassc05694f2013-04-03 11:07:16 +00001023 for diff, name in delta:
1024 if diff:
1025 color = self.col.RED if diff > 0 else self.col.GREEN
1026 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1027 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass02811582022-01-29 14:14:18 -07001028 tprint(msg, colour=color)
Simon Glassc05694f2013-04-03 11:07:16 +00001029
1030
1031 def PrintSizeDetail(self, target_list, show_bloat):
1032 """Show details size information for each board
1033
1034 Args:
1035 target_list: List of targets, each a dict containing:
1036 'target': Target name
1037 'total_diff': Total difference in bytes across all areas
1038 <part_name>: Difference for that part
1039 show_bloat: Show detail for each function
1040 """
1041 targets_by_diff = sorted(target_list, reverse=True,
1042 key=lambda x: x['_total_diff'])
1043 for result in targets_by_diff:
1044 printed_target = False
1045 for name in sorted(result):
1046 diff = result[name]
1047 if name.startswith('_'):
1048 continue
1049 if diff != 0:
1050 color = self.col.RED if diff > 0 else self.col.GREEN
1051 msg = ' %s %+d' % (name, diff)
1052 if not printed_target:
Simon Glass02811582022-01-29 14:14:18 -07001053 tprint('%10s %-15s:' % ('', result['_target']),
Simon Glass4433aa92014-09-05 19:00:07 -06001054 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001055 printed_target = True
Simon Glass02811582022-01-29 14:14:18 -07001056 tprint(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001057 if printed_target:
Simon Glass02811582022-01-29 14:14:18 -07001058 tprint()
Simon Glassc05694f2013-04-03 11:07:16 +00001059 if show_bloat:
1060 target = result['_target']
1061 outcome = result['_outcome']
1062 base_outcome = self._base_board_dict[target]
1063 for fname in outcome.func_sizes:
1064 self.PrintFuncSizeDetail(fname,
1065 base_outcome.func_sizes[fname],
1066 outcome.func_sizes[fname])
1067
1068
1069 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1070 show_bloat):
1071 """Print a summary of image sizes broken down by section.
1072
1073 The summary takes the form of one line per architecture. The
1074 line contains deltas for each of the sections (+ means the section
Flavio Suligoie5e3c112020-01-29 09:56:05 +01001075 got bigger, - means smaller). The numbers are the average number
Simon Glassc05694f2013-04-03 11:07:16 +00001076 of bytes that a board in this section increased by.
1077
1078 For example:
1079 powerpc: (622 boards) text -0.0
1080 arm: (285 boards) text -0.0
Simon Glassc05694f2013-04-03 11:07:16 +00001081
1082 Args:
1083 board_selected: Dict containing boards to summarise, keyed by
1084 board.target
1085 board_dict: Dict containing boards for which we built this
1086 commit, keyed by board.target. The value is an Outcome object.
Simon Glassb4002462020-03-18 09:42:43 -06001087 show_detail: Show size delta detail for each board
Simon Glassc05694f2013-04-03 11:07:16 +00001088 show_bloat: Show detail for each function
1089 """
1090 arch_list = {}
1091 arch_count = {}
1092
1093 # Calculate changes in size for different image parts
1094 # The previous sizes are in Board.sizes, for each board
1095 for target in board_dict:
1096 if target not in board_selected:
1097 continue
1098 base_sizes = self._base_board_dict[target].sizes
1099 outcome = board_dict[target]
1100 sizes = outcome.sizes
1101
1102 # Loop through the list of images, creating a dict of size
1103 # changes for each image/part. We end up with something like
1104 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1105 # which means that U-Boot data increased by 5 bytes and SPL
1106 # text decreased by 4.
1107 err = {'_target' : target}
1108 for image in sizes:
1109 if image in base_sizes:
1110 base_image = base_sizes[image]
1111 # Loop through the text, data, bss parts
1112 for part in sorted(sizes[image]):
1113 diff = sizes[image][part] - base_image[part]
1114 col = None
1115 if diff:
1116 if image == 'u-boot':
1117 name = part
1118 else:
1119 name = image + ':' + part
1120 err[name] = diff
1121 arch = board_selected[target].arch
1122 if not arch in arch_count:
1123 arch_count[arch] = 1
1124 else:
1125 arch_count[arch] += 1
1126 if not sizes:
1127 pass # Only add to our list when we have some stats
1128 elif not arch in arch_list:
1129 arch_list[arch] = [err]
1130 else:
1131 arch_list[arch].append(err)
1132
1133 # We now have a list of image size changes sorted by arch
1134 # Print out a summary of these
Simon Glassc78ed662019-10-31 07:42:53 -06001135 for arch, target_list in arch_list.items():
Simon Glassc05694f2013-04-03 11:07:16 +00001136 # Get total difference for each type
1137 totals = {}
1138 for result in target_list:
1139 total = 0
Simon Glassc78ed662019-10-31 07:42:53 -06001140 for name, diff in result.items():
Simon Glassc05694f2013-04-03 11:07:16 +00001141 if name.startswith('_'):
1142 continue
1143 total += diff
1144 if name in totals:
1145 totals[name] += diff
1146 else:
1147 totals[name] = diff
1148 result['_total_diff'] = total
1149 result['_outcome'] = board_dict[result['_target']]
1150
1151 count = len(target_list)
1152 printed_arch = False
1153 for name in sorted(totals):
1154 diff = totals[name]
1155 if diff:
1156 # Display the average difference in this name for this
1157 # architecture
1158 avg_diff = float(diff) / count
1159 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1160 msg = ' %s %+1.1f' % (name, avg_diff)
1161 if not printed_arch:
Simon Glass02811582022-01-29 14:14:18 -07001162 tprint('%10s: (for %d/%d boards)' % (arch, count,
Simon Glass4433aa92014-09-05 19:00:07 -06001163 arch_count[arch]), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001164 printed_arch = True
Simon Glass02811582022-01-29 14:14:18 -07001165 tprint(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001166
1167 if printed_arch:
Simon Glass02811582022-01-29 14:14:18 -07001168 tprint()
Simon Glassc05694f2013-04-03 11:07:16 +00001169 if show_detail:
1170 self.PrintSizeDetail(target_list, show_bloat)
1171
1172
1173 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glass03749d42014-08-28 09:43:44 -06001174 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001175 config, environment, show_sizes, show_detail,
1176 show_bloat, show_config, show_environment):
Simon Glassc05694f2013-04-03 11:07:16 +00001177 """Compare results with the base results and display delta.
1178
1179 Only boards mentioned in board_selected will be considered. This
1180 function is intended to be called repeatedly with the results of
1181 each commit. It therefore shows a 'diff' between what it saw in
1182 the last call and what it sees now.
1183
1184 Args:
1185 board_selected: Dict containing boards to summarise, keyed by
1186 board.target
1187 board_dict: Dict containing boards for which we built this
1188 commit, keyed by board.target. The value is an Outcome object.
1189 err_lines: A list of errors for this commit, or [] if there is
1190 none, or we don't want to print errors
Simon Glass3394c9f2014-08-28 09:43:43 -06001191 err_line_boards: Dict keyed by error line, containing a list of
1192 the Board objects with that error
Simon Glass03749d42014-08-28 09:43:44 -06001193 warn_lines: A list of warnings for this commit, or [] if there is
1194 none, or we don't want to print errors
1195 warn_line_boards: Dict keyed by warning line, containing a list of
1196 the Board objects with that warning
Simon Glassdb17fb82015-02-05 22:06:15 -07001197 config: Dictionary keyed by filename - e.g. '.config'. Each
1198 value is itself a dictionary:
1199 key: config name
1200 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +00001201 environment: Dictionary keyed by environment variable, Each
1202 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +00001203 show_sizes: Show image size deltas
Simon Glassb4002462020-03-18 09:42:43 -06001204 show_detail: Show size delta detail for each board if show_sizes
Simon Glassc05694f2013-04-03 11:07:16 +00001205 show_bloat: Show detail for each function
Simon Glassdb17fb82015-02-05 22:06:15 -07001206 show_config: Show config changes
Alex Kiernan4059e302018-05-31 04:48:34 +00001207 show_environment: Show environment changes
Simon Glassc05694f2013-04-03 11:07:16 +00001208 """
Simon Glass03749d42014-08-28 09:43:44 -06001209 def _BoardList(line, line_boards):
Simon Glass3394c9f2014-08-28 09:43:43 -06001210 """Helper function to get a line of boards containing a line
1211
1212 Args:
1213 line: Error line to search for
Simon Glassde0fefc2020-04-09 15:08:36 -06001214 line_boards: boards to search, each a Board
Simon Glass3394c9f2014-08-28 09:43:43 -06001215 Return:
Simon Glassde0fefc2020-04-09 15:08:36 -06001216 List of boards with that error line, or [] if the user has not
1217 requested such a list
Simon Glass3394c9f2014-08-28 09:43:43 -06001218 """
Simon Glass5df45222022-07-11 19:04:00 -06001219 brds = []
Simon Glassde0fefc2020-04-09 15:08:36 -06001220 board_set = set()
Simon Glass3394c9f2014-08-28 09:43:43 -06001221 if self._list_error_boards:
Simon Glass8132f982022-07-11 19:03:57 -06001222 for brd in line_boards[line]:
1223 if not brd in board_set:
Simon Glass5df45222022-07-11 19:04:00 -06001224 brds.append(brd)
Simon Glass8132f982022-07-11 19:03:57 -06001225 board_set.add(brd)
Simon Glass5df45222022-07-11 19:04:00 -06001226 return brds
Simon Glass3394c9f2014-08-28 09:43:43 -06001227
Simon Glass03749d42014-08-28 09:43:44 -06001228 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1229 char):
Simon Glassde0fefc2020-04-09 15:08:36 -06001230 """Calculate the required output based on changes in errors
1231
1232 Args:
1233 base_lines: List of errors/warnings for previous commit
1234 base_line_boards: Dict keyed by error line, containing a list
1235 of the Board objects with that error in the previous commit
1236 lines: List of errors/warning for this commit, each a str
1237 line_boards: Dict keyed by error line, containing a list
1238 of the Board objects with that error in this commit
1239 char: Character representing error ('') or warning ('w'). The
1240 broken ('+') or fixed ('-') characters are added in this
1241 function
1242
1243 Returns:
1244 Tuple
1245 List of ErrLine objects for 'better' lines
1246 List of ErrLine objects for 'worse' lines
1247 """
Simon Glass03749d42014-08-28 09:43:44 -06001248 better_lines = []
1249 worse_lines = []
1250 for line in lines:
1251 if line not in base_lines:
Simon Glassde0fefc2020-04-09 15:08:36 -06001252 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1253 line)
1254 worse_lines.append(errline)
Simon Glass03749d42014-08-28 09:43:44 -06001255 for line in base_lines:
1256 if line not in lines:
Simon Glassde0fefc2020-04-09 15:08:36 -06001257 errline = ErrLine(char + '-',
1258 _BoardList(line, base_line_boards), line)
1259 better_lines.append(errline)
Simon Glass03749d42014-08-28 09:43:44 -06001260 return better_lines, worse_lines
1261
Simon Glassdb17fb82015-02-05 22:06:15 -07001262 def _CalcConfig(delta, name, config):
1263 """Calculate configuration changes
1264
1265 Args:
1266 delta: Type of the delta, e.g. '+'
1267 name: name of the file which changed (e.g. .config)
1268 config: configuration change dictionary
1269 key: config name
1270 value: config value
1271 Returns:
1272 String containing the configuration changes which can be
1273 printed
1274 """
1275 out = ''
1276 for key in sorted(config.keys()):
1277 out += '%s=%s ' % (key, config[key])
Simon Glasscad8abf2015-08-25 21:52:14 -06001278 return '%s %s: %s' % (delta, name, out)
Simon Glassdb17fb82015-02-05 22:06:15 -07001279
Simon Glasscad8abf2015-08-25 21:52:14 -06001280 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1281 """Add changes in configuration to a list
Simon Glassdb17fb82015-02-05 22:06:15 -07001282
1283 Args:
Simon Glasscad8abf2015-08-25 21:52:14 -06001284 lines: list to add to
1285 name: config file name
Simon Glassdb17fb82015-02-05 22:06:15 -07001286 config_plus: configurations added, dictionary
1287 key: config name
1288 value: config value
1289 config_minus: configurations removed, dictionary
1290 key: config name
1291 value: config value
1292 config_change: configurations changed, dictionary
1293 key: config name
1294 value: config value
1295 """
1296 if config_plus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001297 lines.append(_CalcConfig('+', name, config_plus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001298 if config_minus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001299 lines.append(_CalcConfig('-', name, config_minus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001300 if config_change:
Simon Glasscad8abf2015-08-25 21:52:14 -06001301 lines.append(_CalcConfig('c', name, config_change))
1302
1303 def _OutputConfigInfo(lines):
1304 for line in lines:
1305 if not line:
1306 continue
1307 if line[0] == '+':
1308 col = self.col.GREEN
1309 elif line[0] == '-':
1310 col = self.col.RED
1311 elif line[0] == 'c':
1312 col = self.col.YELLOW
Simon Glass02811582022-01-29 14:14:18 -07001313 tprint(' ' + line, newline=True, colour=col)
Simon Glasscad8abf2015-08-25 21:52:14 -06001314
Simon Glassac500222020-04-09 15:08:28 -06001315 def _OutputErrLines(err_lines, colour):
1316 """Output the line of error/warning lines, if not empty
1317
1318 Also increments self._error_lines if err_lines not empty
1319
1320 Args:
Simon Glassde0fefc2020-04-09 15:08:36 -06001321 err_lines: List of ErrLine objects, each an error or warning
1322 line, possibly including a list of boards with that
1323 error/warning
Simon Glassac500222020-04-09 15:08:28 -06001324 colour: Colour to use for output
1325 """
1326 if err_lines:
Simon Glassea49f9b2020-04-09 15:08:37 -06001327 out_list = []
Simon Glassde0fefc2020-04-09 15:08:36 -06001328 for line in err_lines:
Simon Glass5df45222022-07-11 19:04:00 -06001329 names = [brd.target for brd in line.brds]
Simon Glass070589b2020-04-09 15:08:38 -06001330 board_str = ' '.join(names) if names else ''
Simon Glassea49f9b2020-04-09 15:08:37 -06001331 if board_str:
Simon Glassf45d3742022-01-29 14:14:17 -07001332 out = self.col.build(colour, line.char + '(')
1333 out += self.col.build(self.col.MAGENTA, board_str,
Simon Glassea49f9b2020-04-09 15:08:37 -06001334 bright=False)
Simon Glassf45d3742022-01-29 14:14:17 -07001335 out += self.col.build(colour, ') %s' % line.errline)
Simon Glassea49f9b2020-04-09 15:08:37 -06001336 else:
Simon Glassf45d3742022-01-29 14:14:17 -07001337 out = self.col.build(colour, line.char + line.errline)
Simon Glassea49f9b2020-04-09 15:08:37 -06001338 out_list.append(out)
Simon Glass02811582022-01-29 14:14:18 -07001339 tprint('\n'.join(out_list))
Simon Glassac500222020-04-09 15:08:28 -06001340 self._error_lines += 1
1341
Simon Glassdb17fb82015-02-05 22:06:15 -07001342
Simon Glass454507f2018-11-06 16:02:12 -07001343 ok_boards = [] # List of boards fixed since last commit
Simon Glass071a1782018-11-06 16:02:13 -07001344 warn_boards = [] # List of boards with warnings since last commit
Simon Glass454507f2018-11-06 16:02:12 -07001345 err_boards = [] # List of new broken boards since last commit
1346 new_boards = [] # List of boards that didn't exist last time
1347 unknown_boards = [] # List of boards that were not built
Simon Glassc05694f2013-04-03 11:07:16 +00001348
1349 for target in board_dict:
1350 if target not in board_selected:
1351 continue
1352
1353 # If the board was built last time, add its outcome to a list
1354 if target in self._base_board_dict:
1355 base_outcome = self._base_board_dict[target].rc
1356 outcome = board_dict[target]
1357 if outcome.rc == OUTCOME_UNKNOWN:
Simon Glass454507f2018-11-06 16:02:12 -07001358 unknown_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001359 elif outcome.rc < base_outcome:
Simon Glass071a1782018-11-06 16:02:13 -07001360 if outcome.rc == OUTCOME_WARNING:
1361 warn_boards.append(target)
1362 else:
1363 ok_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001364 elif outcome.rc > base_outcome:
Simon Glass071a1782018-11-06 16:02:13 -07001365 if outcome.rc == OUTCOME_WARNING:
1366 warn_boards.append(target)
1367 else:
1368 err_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001369 else:
Simon Glass454507f2018-11-06 16:02:12 -07001370 new_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001371
Simon Glassac500222020-04-09 15:08:28 -06001372 # Get a list of errors and warnings that have appeared, and disappeared
Simon Glass03749d42014-08-28 09:43:44 -06001373 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1374 self._base_err_line_boards, err_lines, err_line_boards, '')
1375 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1376 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassc05694f2013-04-03 11:07:16 +00001377
Simon Glass6c435622022-07-11 19:03:56 -06001378 # For the IDE mode, print out all the output
1379 if self._ide:
1380 outcome = board_dict[target]
1381 for line in outcome.err_lines:
1382 sys.stderr.write(line)
1383
Simon Glassc05694f2013-04-03 11:07:16 +00001384 # Display results by arch
Simon Glass6c435622022-07-11 19:03:56 -06001385 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
Simon Glass071a1782018-11-06 16:02:13 -07001386 worse_err, better_err, worse_warn, better_warn)):
Simon Glassc05694f2013-04-03 11:07:16 +00001387 arch_list = {}
Simon Glass454507f2018-11-06 16:02:12 -07001388 self.AddOutcome(board_selected, arch_list, ok_boards, '',
Simon Glassc05694f2013-04-03 11:07:16 +00001389 self.col.GREEN)
Simon Glass071a1782018-11-06 16:02:13 -07001390 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1391 self.col.YELLOW)
Simon Glass454507f2018-11-06 16:02:12 -07001392 self.AddOutcome(board_selected, arch_list, err_boards, '+',
Simon Glassc05694f2013-04-03 11:07:16 +00001393 self.col.RED)
Simon Glass454507f2018-11-06 16:02:12 -07001394 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
Simon Glassc05694f2013-04-03 11:07:16 +00001395 if self._show_unknown:
Simon Glass454507f2018-11-06 16:02:12 -07001396 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
Simon Glassc05694f2013-04-03 11:07:16 +00001397 self.col.MAGENTA)
Simon Glassc78ed662019-10-31 07:42:53 -06001398 for arch, target_list in arch_list.items():
Simon Glass02811582022-01-29 14:14:18 -07001399 tprint('%10s: %s' % (arch, target_list))
Simon Glassbb4dffb2014-08-09 15:33:06 -06001400 self._error_lines += 1
Simon Glassac500222020-04-09 15:08:28 -06001401 _OutputErrLines(better_err, colour=self.col.GREEN)
1402 _OutputErrLines(worse_err, colour=self.col.RED)
1403 _OutputErrLines(better_warn, colour=self.col.CYAN)
Simon Glass564ddac2020-04-09 15:08:35 -06001404 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
Simon Glassc05694f2013-04-03 11:07:16 +00001405
1406 if show_sizes:
1407 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1408 show_bloat)
1409
Alex Kiernan4059e302018-05-31 04:48:34 +00001410 if show_environment and self._base_environment:
1411 lines = []
1412
1413 for target in board_dict:
1414 if target not in board_selected:
1415 continue
1416
1417 tbase = self._base_environment[target]
1418 tenvironment = environment[target]
1419 environment_plus = {}
1420 environment_minus = {}
1421 environment_change = {}
1422 base = tbase.environment
Simon Glassc78ed662019-10-31 07:42:53 -06001423 for key, value in tenvironment.environment.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001424 if key not in base:
1425 environment_plus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001426 for key, value in base.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001427 if key not in tenvironment.environment:
1428 environment_minus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001429 for key, value in base.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001430 new_value = tenvironment.environment.get(key)
1431 if new_value and value != new_value:
1432 desc = '%s -> %s' % (value, new_value)
1433 environment_change[key] = desc
1434
1435 _AddConfig(lines, target, environment_plus, environment_minus,
1436 environment_change)
1437
1438 _OutputConfigInfo(lines)
1439
Simon Glasscad8abf2015-08-25 21:52:14 -06001440 if show_config and self._base_config:
1441 summary = {}
1442 arch_config_plus = {}
1443 arch_config_minus = {}
1444 arch_config_change = {}
1445 arch_list = []
1446
1447 for target in board_dict:
1448 if target not in board_selected:
1449 continue
1450 arch = board_selected[target].arch
1451 if arch not in arch_list:
1452 arch_list.append(arch)
1453
1454 for arch in arch_list:
1455 arch_config_plus[arch] = {}
1456 arch_config_minus[arch] = {}
1457 arch_config_change[arch] = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001458 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001459 arch_config_plus[arch][name] = {}
1460 arch_config_minus[arch][name] = {}
1461 arch_config_change[arch][name] = {}
1462
1463 for target in board_dict:
1464 if target not in board_selected:
Simon Glassdb17fb82015-02-05 22:06:15 -07001465 continue
Simon Glasscad8abf2015-08-25 21:52:14 -06001466
1467 arch = board_selected[target].arch
1468
1469 all_config_plus = {}
1470 all_config_minus = {}
1471 all_config_change = {}
1472 tbase = self._base_config[target]
1473 tconfig = config[target]
1474 lines = []
Simon Glasscde5c302016-11-13 14:25:53 -07001475 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001476 if not tconfig.config[name]:
1477 continue
1478 config_plus = {}
1479 config_minus = {}
1480 config_change = {}
1481 base = tbase.config[name]
Simon Glassc78ed662019-10-31 07:42:53 -06001482 for key, value in tconfig.config[name].items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001483 if key not in base:
1484 config_plus[key] = value
1485 all_config_plus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001486 for key, value in base.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001487 if key not in tconfig.config[name]:
1488 config_minus[key] = value
1489 all_config_minus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001490 for key, value in base.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001491 new_value = tconfig.config.get(key)
1492 if new_value and value != new_value:
1493 desc = '%s -> %s' % (value, new_value)
1494 config_change[key] = desc
1495 all_config_change[key] = desc
1496
1497 arch_config_plus[arch][name].update(config_plus)
1498 arch_config_minus[arch][name].update(config_minus)
1499 arch_config_change[arch][name].update(config_change)
1500
1501 _AddConfig(lines, name, config_plus, config_minus,
1502 config_change)
1503 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1504 all_config_change)
1505 summary[target] = '\n'.join(lines)
1506
1507 lines_by_target = {}
Simon Glassc78ed662019-10-31 07:42:53 -06001508 for target, lines in summary.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001509 if lines in lines_by_target:
1510 lines_by_target[lines].append(target)
1511 else:
1512 lines_by_target[lines] = [target]
1513
1514 for arch in arch_list:
1515 lines = []
1516 all_plus = {}
1517 all_minus = {}
1518 all_change = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001519 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001520 all_plus.update(arch_config_plus[arch][name])
1521 all_minus.update(arch_config_minus[arch][name])
1522 all_change.update(arch_config_change[arch][name])
1523 _AddConfig(lines, name, arch_config_plus[arch][name],
1524 arch_config_minus[arch][name],
1525 arch_config_change[arch][name])
1526 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1527 #arch_summary[target] = '\n'.join(lines)
1528 if lines:
Simon Glass02811582022-01-29 14:14:18 -07001529 tprint('%s:' % arch)
Simon Glasscad8abf2015-08-25 21:52:14 -06001530 _OutputConfigInfo(lines)
1531
Simon Glassc78ed662019-10-31 07:42:53 -06001532 for lines, targets in lines_by_target.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001533 if not lines:
1534 continue
Simon Glass02811582022-01-29 14:14:18 -07001535 tprint('%s :' % ' '.join(sorted(targets)))
Simon Glasscad8abf2015-08-25 21:52:14 -06001536 _OutputConfigInfo(lines.split('\n'))
1537
Simon Glassdb17fb82015-02-05 22:06:15 -07001538
Simon Glassc05694f2013-04-03 11:07:16 +00001539 # Save our updated information for the next call to this function
1540 self._base_board_dict = board_dict
1541 self._base_err_lines = err_lines
Simon Glass03749d42014-08-28 09:43:44 -06001542 self._base_warn_lines = warn_lines
1543 self._base_err_line_boards = err_line_boards
1544 self._base_warn_line_boards = warn_line_boards
Simon Glassdb17fb82015-02-05 22:06:15 -07001545 self._base_config = config
Alex Kiernan4059e302018-05-31 04:48:34 +00001546 self._base_environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +00001547
1548 # Get a list of boards that did not get built, if needed
1549 not_built = []
Simon Glass8132f982022-07-11 19:03:57 -06001550 for brd in board_selected:
1551 if not brd in board_dict:
1552 not_built.append(brd)
Simon Glassc05694f2013-04-03 11:07:16 +00001553 if not_built:
Simon Glass02811582022-01-29 14:14:18 -07001554 tprint("Boards not built (%d): %s" % (len(not_built),
Simon Glass4433aa92014-09-05 19:00:07 -06001555 ', '.join(not_built)))
Simon Glassc05694f2013-04-03 11:07:16 +00001556
Simon Glasseb48bbc2014-08-09 15:33:02 -06001557 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glass03749d42014-08-28 09:43:44 -06001558 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan4059e302018-05-31 04:48:34 +00001559 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glass3394c9f2014-08-28 09:43:43 -06001560 board_selected, commit_upto,
Simon Glassdb17fb82015-02-05 22:06:15 -07001561 read_func_sizes=self._show_bloat,
Alex Kiernan4059e302018-05-31 04:48:34 +00001562 read_config=self._show_config,
1563 read_environment=self._show_environment)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001564 if commits:
1565 msg = '%02d: %s' % (commit_upto + 1,
1566 commits[commit_upto].subject)
Simon Glass02811582022-01-29 14:14:18 -07001567 tprint(msg, colour=self.col.BLUE)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001568 self.PrintResultSummary(board_selected, board_dict,
Simon Glass3394c9f2014-08-28 09:43:43 -06001569 err_lines if self._show_errors else [], err_line_boards,
Simon Glass03749d42014-08-28 09:43:44 -06001570 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001571 config, environment, self._show_sizes, self._show_detail,
1572 self._show_bloat, self._show_config, self._show_environment)
Simon Glassc05694f2013-04-03 11:07:16 +00001573
Simon Glasseb48bbc2014-08-09 15:33:02 -06001574 def ShowSummary(self, commits, board_selected):
Simon Glassc05694f2013-04-03 11:07:16 +00001575 """Show a build summary for U-Boot for a given board list.
1576
1577 Reset the result summary, then repeatedly call GetResultSummary on
1578 each commit's results, then display the differences we see.
1579
1580 Args:
1581 commit: Commit objects to summarise
1582 board_selected: Dict containing boards to summarise
Simon Glassc05694f2013-04-03 11:07:16 +00001583 """
Simon Glassd326ad72014-08-09 15:32:59 -06001584 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001585 self.commits = commits
1586 self.ResetResultSummary(board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001587 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +00001588
1589 for commit_upto in range(0, self.commit_count, self._step):
Simon Glasseb48bbc2014-08-09 15:33:02 -06001590 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001591 if not self._error_lines:
Simon Glass02811582022-01-29 14:14:18 -07001592 tprint('(no errors to report)', colour=self.col.GREEN)
Simon Glassc05694f2013-04-03 11:07:16 +00001593
1594
1595 def SetupBuild(self, board_selected, commits):
1596 """Set up ready to start a build.
1597
1598 Args:
1599 board_selected: Selected boards to build
1600 commits: Selected commits to build
1601 """
1602 # First work out how many commits we will build
Simon Glassc78ed662019-10-31 07:42:53 -06001603 count = (self.commit_count + self._step - 1) // self._step
Simon Glassc05694f2013-04-03 11:07:16 +00001604 self.count = len(board_selected) * count
1605 self.upto = self.warned = self.fail = 0
1606 self._timestamps = collections.deque()
1607
Simon Glassc05694f2013-04-03 11:07:16 +00001608 def GetThreadDir(self, thread_num):
1609 """Get the directory path to the working dir for a thread.
1610
1611 Args:
Simon Glassc635d892021-01-30 22:17:46 -07001612 thread_num: Number of thread to check (-1 for main process, which
1613 is treated as 0)
Simon Glassc05694f2013-04-03 11:07:16 +00001614 """
Simon Glassb6eb8cf2020-03-18 09:42:42 -06001615 if self.work_in_output:
1616 return self._working_dir
Simon Glassc635d892021-01-30 22:17:46 -07001617 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
Simon Glassc05694f2013-04-03 11:07:16 +00001618
Simon Glassd326ad72014-08-09 15:32:59 -06001619 def _PrepareThread(self, thread_num, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001620 """Prepare the working directory for a thread.
1621
1622 This clones or fetches the repo into the thread's work directory.
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001623 Optionally, it can create a linked working tree of the repo in the
1624 thread's work directory instead.
Simon Glassc05694f2013-04-03 11:07:16 +00001625
1626 Args:
1627 thread_num: Thread number (0, 1, ...)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001628 setup_git:
1629 'clone' to set up a git clone
1630 'worktree' to set up a git worktree
Simon Glassc05694f2013-04-03 11:07:16 +00001631 """
1632 thread_dir = self.GetThreadDir(thread_num)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001633 builderthread.Mkdir(thread_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001634 git_dir = os.path.join(thread_dir, '.git')
1635
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001636 # Create a worktree or a git repo clone for this thread if it
1637 # doesn't already exist
Simon Glassd326ad72014-08-09 15:32:59 -06001638 if setup_git and self.git_dir:
Simon Glassc05694f2013-04-03 11:07:16 +00001639 src_dir = os.path.abspath(self.git_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001640 if os.path.isdir(git_dir):
1641 # This is a clone of the src_dir repo, we can keep using
1642 # it but need to fetch from src_dir.
Simon Glass02811582022-01-29 14:14:18 -07001643 tprint('\rFetching repo for thread %d' % thread_num,
Simon Glass43054932020-04-09 15:08:43 -06001644 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001645 gitutil.fetch(git_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001646 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001647 elif os.path.isfile(git_dir):
1648 # This is a worktree of the src_dir repo, we don't need to
1649 # create it again or update it in any way.
1650 pass
1651 elif os.path.exists(git_dir):
1652 # Don't know what could trigger this, but we probably
1653 # can't create a git worktree/clone here.
1654 raise ValueError('Git dir %s exists, but is not a file '
1655 'or a directory.' % git_dir)
1656 elif setup_git == 'worktree':
Simon Glass02811582022-01-29 14:14:18 -07001657 tprint('\rChecking out worktree for thread %d' % thread_num,
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001658 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001659 gitutil.add_worktree(src_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001660 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001661 elif setup_git == 'clone' or setup_git == True:
Simon Glass02811582022-01-29 14:14:18 -07001662 tprint('\rCloning repo for thread %d' % thread_num,
Simon Glass2e2a6e62016-09-18 16:48:31 -06001663 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001664 gitutil.clone(src_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001665 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001666 else:
1667 raise ValueError("Can't setup git repo with %s." % setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001668
Simon Glassd326ad72014-08-09 15:32:59 -06001669 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001670 """Prepare the working directory for use.
1671
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001672 Set up the git repo for each thread. Creates a linked working tree
1673 if git-worktree is available, or clones the repo if it isn't.
Simon Glassc05694f2013-04-03 11:07:16 +00001674
1675 Args:
Simon Glassc635d892021-01-30 22:17:46 -07001676 max_threads: Maximum number of threads we expect to need. If 0 then
1677 1 is set up, since the main process still needs somewhere to
1678 work
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001679 setup_git: True to set up a git worktree or a git clone
Simon Glassc05694f2013-04-03 11:07:16 +00001680 """
Simon Glass4a1e88b2014-08-09 15:33:00 -06001681 builderthread.Mkdir(self._working_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001682 if setup_git and self.git_dir:
1683 src_dir = os.path.abspath(self.git_dir)
Simon Glass761648b2022-01-29 14:14:11 -07001684 if gitutil.check_worktree_is_available(src_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001685 setup_git = 'worktree'
1686 # If we previously added a worktree but the directory for it
1687 # got deleted, we need to prune its files from the repo so
1688 # that we can check out another in its place.
Simon Glass761648b2022-01-29 14:14:11 -07001689 gitutil.prune_worktrees(src_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001690 else:
1691 setup_git = 'clone'
Simon Glassc635d892021-01-30 22:17:46 -07001692
1693 # Always do at least one thread
1694 for thread in range(max(max_threads, 1)):
Simon Glassd326ad72014-08-09 15:32:59 -06001695 self._PrepareThread(thread, setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001696
Simon Glass5dc1ca72020-03-18 09:42:45 -06001697 def _GetOutputSpaceRemovals(self):
Simon Glassc05694f2013-04-03 11:07:16 +00001698 """Get the output directories ready to receive files.
1699
Simon Glass5dc1ca72020-03-18 09:42:45 -06001700 Figure out what needs to be deleted in the output directory before it
1701 can be used. We only delete old buildman directories which have the
1702 expected name pattern. See _GetOutputDir().
1703
1704 Returns:
1705 List of full paths of directories to remove
Simon Glassc05694f2013-04-03 11:07:16 +00001706 """
Simon Glassb9a9b7c2014-12-01 17:33:53 -07001707 if not self.commits:
1708 return
Simon Glassc05694f2013-04-03 11:07:16 +00001709 dir_list = []
1710 for commit_upto in range(self.commit_count):
1711 dir_list.append(self._GetOutputDir(commit_upto))
1712
Simon Glass83cb6cc2016-09-18 16:48:32 -06001713 to_remove = []
Simon Glassc05694f2013-04-03 11:07:16 +00001714 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1715 if dirname not in dir_list:
Simon Glass5dc1ca72020-03-18 09:42:45 -06001716 leaf = dirname[len(self.base_dir) + 1:]
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +03001717 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
Simon Glass5dc1ca72020-03-18 09:42:45 -06001718 if m:
1719 to_remove.append(dirname)
1720 return to_remove
1721
1722 def _PrepareOutputSpace(self):
1723 """Get the output directories ready to receive files.
1724
1725 We delete any output directories which look like ones we need to
1726 create. Having left over directories is confusing when the user wants
1727 to check the output manually.
1728 """
1729 to_remove = self._GetOutputSpaceRemovals()
Simon Glass83cb6cc2016-09-18 16:48:32 -06001730 if to_remove:
Simon Glass02811582022-01-29 14:14:18 -07001731 tprint('Removing %d old build directories...' % len(to_remove),
Simon Glass83cb6cc2016-09-18 16:48:32 -06001732 newline=False)
1733 for dirname in to_remove:
Simon Glassc05694f2013-04-03 11:07:16 +00001734 shutil.rmtree(dirname)
Simon Glass02811582022-01-29 14:14:18 -07001735 terminal.print_clear()
Simon Glassc05694f2013-04-03 11:07:16 +00001736
Simon Glass78e418e2014-08-09 15:33:03 -06001737 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +00001738 """Build all commits for a list of boards
1739
1740 Args:
1741 commits: List of commits to be build, each a Commit object
1742 boards_selected: Dict of selected boards, key is target name,
1743 value is Board object
Simon Glassc05694f2013-04-03 11:07:16 +00001744 keep_outputs: True to save build output files
Simon Glass78e418e2014-08-09 15:33:03 -06001745 verbose: Display build results as they are completed
Simon Glassc2f91072014-08-28 09:43:39 -06001746 Returns:
1747 Tuple containing:
1748 - number of boards that failed to build
1749 - number of boards that issued warnings
Simon Glass9bf9a722021-04-11 16:27:27 +12001750 - list of thread exceptions raised
Simon Glassc05694f2013-04-03 11:07:16 +00001751 """
Simon Glassd326ad72014-08-09 15:32:59 -06001752 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001753 self.commits = commits
Simon Glass78e418e2014-08-09 15:33:03 -06001754 self._verbose = verbose
Simon Glassc05694f2013-04-03 11:07:16 +00001755
1756 self.ResetResultSummary(board_selected)
Thierry Reding336d5bd2014-08-19 10:22:39 +02001757 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassd326ad72014-08-09 15:32:59 -06001758 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1759 commits is not None)
Simon Glassc05694f2013-04-03 11:07:16 +00001760 self._PrepareOutputSpace()
Simon Glass6c435622022-07-11 19:03:56 -06001761 if not self._ide:
1762 tprint('\rStarting build...', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001763 self.SetupBuild(board_selected, commits)
1764 self.ProcessResult(None)
Simon Glass9bf9a722021-04-11 16:27:27 +12001765 self.thread_exceptions = []
Simon Glassc05694f2013-04-03 11:07:16 +00001766 # Create jobs to build all commits for each board
Simon Glassc78ed662019-10-31 07:42:53 -06001767 for brd in board_selected.values():
Simon Glass4a1e88b2014-08-09 15:33:00 -06001768 job = builderthread.BuilderJob()
Simon Glass8132f982022-07-11 19:03:57 -06001769 job.brd = brd
Simon Glassc05694f2013-04-03 11:07:16 +00001770 job.commits = commits
1771 job.keep_outputs = keep_outputs
Simon Glassb6eb8cf2020-03-18 09:42:42 -06001772 job.work_in_output = self.work_in_output
Simon Glasse5650a82022-01-22 05:07:33 -07001773 job.adjust_cfg = self.adjust_cfg
Simon Glassc05694f2013-04-03 11:07:16 +00001774 job.step = self._step
Simon Glassc635d892021-01-30 22:17:46 -07001775 if self.num_threads:
1776 self.queue.put(job)
1777 else:
Simon Glasse36fe012022-02-11 13:23:19 -07001778 self._single_builder.RunJob(job)
Simon Glassc05694f2013-04-03 11:07:16 +00001779
Simon Glassc635d892021-01-30 22:17:46 -07001780 if self.num_threads:
1781 term = threading.Thread(target=self.queue.join)
1782 term.setDaemon(True)
1783 term.start()
1784 while term.is_alive():
1785 term.join(100)
Simon Glassc05694f2013-04-03 11:07:16 +00001786
Simon Glassc635d892021-01-30 22:17:46 -07001787 # Wait until we have processed all output
1788 self.out_queue.join()
Simon Glass6c435622022-07-11 19:03:56 -06001789 if not self._ide:
1790 tprint()
Simon Glass726ae812020-04-09 15:08:47 -06001791
Simon Glass6c435622022-07-11 19:03:56 -06001792 msg = 'Completed: %d total built' % self.count
1793 if self.already_done:
1794 msg += ' (%d previously' % self.already_done
1795 if self.already_done != self.count:
1796 msg += ', %d newly' % (self.count - self.already_done)
1797 msg += ')'
1798 duration = datetime.now() - self._start_time
1799 if duration > timedelta(microseconds=1000000):
1800 if duration.microseconds >= 500000:
1801 duration = duration + timedelta(seconds=1)
1802 duration = duration - timedelta(microseconds=duration.microseconds)
1803 rate = float(self.count) / duration.total_seconds()
1804 msg += ', duration %s, rate %1.2f' % (duration, rate)
1805 tprint(msg)
1806 if self.thread_exceptions:
1807 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1808 colour=self.col.RED)
Simon Glass726ae812020-04-09 15:08:47 -06001809
Simon Glass9bf9a722021-04-11 16:27:27 +12001810 return (self.fail, self.warned, self.thread_exceptions)