blob: 76252b90792a6c64f20c5b700efbe0ce6d801ef3 [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():
Simon Glass86a2afe2022-07-11 19:04:11 -0600672 line = line.strip()
673 parts = line.split()
674 if line and len(parts) == 3:
675 size, type, name = line.split()
676 if type in 'tTdDbB':
677 # function names begin with '.' on 64-bit powerpc
678 if '.' in name[1:]:
679 name = 'static.' + name.split('.')[0]
680 sym[name] = sym.get(name, 0) + int(size, 16)
Simon Glassc05694f2013-04-03 11:07:16 +0000681 return sym
682
Simon Glassdb17fb82015-02-05 22:06:15 -0700683 def _ProcessConfig(self, fname):
684 """Read in a .config, autoconf.mk or autoconf.h file
685
686 This function handles all config file types. It ignores comments and
687 any #defines which don't start with CONFIG_.
688
689 Args:
690 fname: Filename to read
691
692 Returns:
693 Dictionary:
694 key: Config name (e.g. CONFIG_DM)
695 value: Config value (e.g. 1)
696 """
697 config = {}
698 if os.path.exists(fname):
699 with open(fname) as fd:
700 for line in fd:
701 line = line.strip()
702 if line.startswith('#define'):
703 values = line[8:].split(' ', 1)
704 if len(values) > 1:
705 key, value = values
706 else:
707 key = values[0]
Simon Glasscde5c302016-11-13 14:25:53 -0700708 value = '1' if self.squash_config_y else ''
Simon Glassdb17fb82015-02-05 22:06:15 -0700709 if not key.startswith('CONFIG_'):
710 continue
711 elif not line or line[0] in ['#', '*', '/']:
712 continue
713 else:
714 key, value = line.split('=', 1)
Simon Glasscde5c302016-11-13 14:25:53 -0700715 if self.squash_config_y and value == 'y':
716 value = '1'
Simon Glassdb17fb82015-02-05 22:06:15 -0700717 config[key] = value
718 return config
719
Alex Kiernan4059e302018-05-31 04:48:34 +0000720 def _ProcessEnvironment(self, fname):
721 """Read in a uboot.env file
722
723 This function reads in environment variables from a file.
724
725 Args:
726 fname: Filename to read
727
728 Returns:
729 Dictionary:
730 key: environment variable (e.g. bootlimit)
731 value: value of environment variable (e.g. 1)
732 """
733 environment = {}
734 if os.path.exists(fname):
735 with open(fname) as fd:
736 for line in fd.read().split('\0'):
737 try:
738 key, value = line.split('=', 1)
739 environment[key] = value
740 except ValueError:
741 # ignore lines we can't parse
742 pass
743 return environment
744
Simon Glassdb17fb82015-02-05 22:06:15 -0700745 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000746 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000747 """Work out the outcome of a build.
748
749 Args:
750 commit_upto: Commit number to check (0..n-1)
751 target: Target board to check
752 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700753 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000754 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000755
756 Returns:
757 Outcome object
758 """
759 done_file = self.GetDoneFile(commit_upto, target)
760 sizes_file = self.GetSizesFile(commit_upto, target)
761 sizes = {}
762 func_sizes = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700763 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000764 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000765 if os.path.exists(done_file):
766 with open(done_file, 'r') as fd:
Simon Glass91c54a42019-04-26 19:02:23 -0600767 try:
768 return_code = int(fd.readline())
769 except ValueError:
770 # The file may be empty due to running out of disk space.
771 # Try a rebuild
772 return_code = 1
Simon Glassc05694f2013-04-03 11:07:16 +0000773 err_lines = []
774 err_file = self.GetErrFile(commit_upto, target)
775 if os.path.exists(err_file):
776 with open(err_file, 'r') as fd:
777 err_lines = self.FilterErrors(fd.readlines())
778
779 # Decide whether the build was ok, failed or created warnings
780 if return_code:
781 rc = OUTCOME_ERROR
782 elif len(err_lines):
783 rc = OUTCOME_WARNING
784 else:
785 rc = OUTCOME_OK
786
787 # Convert size information to our simple format
788 if os.path.exists(sizes_file):
789 with open(sizes_file, 'r') as fd:
790 for line in fd.readlines():
791 values = line.split()
792 rodata = 0
793 if len(values) > 6:
794 rodata = int(values[6], 16)
795 size_dict = {
796 'all' : int(values[0]) + int(values[1]) +
797 int(values[2]),
798 'text' : int(values[0]) - rodata,
799 'data' : int(values[1]),
800 'bss' : int(values[2]),
801 'rodata' : rodata,
802 }
803 sizes[values[5]] = size_dict
804
805 if read_func_sizes:
806 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
807 for fname in glob.glob(pattern):
808 with open(fname, 'r') as fd:
809 dict_name = os.path.basename(fname).replace('.sizes',
810 '')
811 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
812
Simon Glassdb17fb82015-02-05 22:06:15 -0700813 if read_config:
814 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glasscde5c302016-11-13 14:25:53 -0700815 for name in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700816 fname = os.path.join(output_dir, name)
817 config[name] = self._ProcessConfig(fname)
818
Alex Kiernan4059e302018-05-31 04:48:34 +0000819 if read_environment:
820 output_dir = self.GetBuildDir(commit_upto, target)
821 fname = os.path.join(output_dir, 'uboot.env')
822 environment = self._ProcessEnvironment(fname)
823
824 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
825 environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000826
Alex Kiernan4059e302018-05-31 04:48:34 +0000827 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000828
Simon Glassdb17fb82015-02-05 22:06:15 -0700829 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000830 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000831 """Calculate a summary of the results of building a commit.
832
833 Args:
834 board_selected: Dict containing boards to summarise
835 commit_upto: Commit number to summarize (0..self.count-1)
836 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700837 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000838 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000839
840 Returns:
841 Tuple:
Simon Glass6c435622022-07-11 19:03:56 -0600842 Dict containing boards which built this commit:
843 key: board.target
844 value: Builder.Outcome object
Simon Glass03749d42014-08-28 09:43:44 -0600845 List containing a summary of error lines
Simon Glass3394c9f2014-08-28 09:43:43 -0600846 Dict keyed by error line, containing a list of the Board
847 objects with that error
Simon Glass03749d42014-08-28 09:43:44 -0600848 List containing a summary of warning lines
849 Dict keyed by error line, containing a list of the Board
850 objects with that warning
Simon Glasscad8abf2015-08-25 21:52:14 -0600851 Dictionary keyed by board.target. Each value is a dictionary:
852 key: filename - e.g. '.config'
Simon Glassdb17fb82015-02-05 22:06:15 -0700853 value is itself a dictionary:
854 key: config name
855 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +0000856 Dictionary keyed by board.target. Each value is a dictionary:
857 key: environment variable
858 value: value of environment variable
Simon Glassc05694f2013-04-03 11:07:16 +0000859 """
Simon Glass03749d42014-08-28 09:43:44 -0600860 def AddLine(lines_summary, lines_boards, line, board):
861 line = line.rstrip()
862 if line in lines_boards:
863 lines_boards[line].append(board)
864 else:
865 lines_boards[line] = [board]
866 lines_summary.append(line)
867
Simon Glassc05694f2013-04-03 11:07:16 +0000868 board_dict = {}
869 err_lines_summary = []
Simon Glass3394c9f2014-08-28 09:43:43 -0600870 err_lines_boards = {}
Simon Glass03749d42014-08-28 09:43:44 -0600871 warn_lines_summary = []
872 warn_lines_boards = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700873 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000874 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000875
Simon Glass8132f982022-07-11 19:03:57 -0600876 for brd in boards_selected.values():
877 outcome = self.GetBuildOutcome(commit_upto, brd.target,
Alex Kiernan4059e302018-05-31 04:48:34 +0000878 read_func_sizes, read_config,
879 read_environment)
Simon Glass8132f982022-07-11 19:03:57 -0600880 board_dict[brd.target] = outcome
Simon Glass03749d42014-08-28 09:43:44 -0600881 last_func = None
882 last_was_warning = False
883 for line in outcome.err_lines:
884 if line:
885 if (self._re_function.match(line) or
886 self._re_files.match(line)):
887 last_func = line
Simon Glass3394c9f2014-08-28 09:43:43 -0600888 else:
Simon Glass0db94432018-11-06 16:02:11 -0700889 is_warning = (self._re_warning.match(line) or
890 self._re_dtb_warning.match(line))
Simon Glass03749d42014-08-28 09:43:44 -0600891 is_note = self._re_note.match(line)
892 if is_warning or (last_was_warning and is_note):
893 if last_func:
894 AddLine(warn_lines_summary, warn_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600895 last_func, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600896 AddLine(warn_lines_summary, warn_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600897 line, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600898 else:
899 if last_func:
900 AddLine(err_lines_summary, err_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600901 last_func, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600902 AddLine(err_lines_summary, err_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600903 line, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600904 last_was_warning = is_warning
905 last_func = None
Simon Glass8132f982022-07-11 19:03:57 -0600906 tconfig = Config(self.config_filenames, brd.target)
Simon Glasscde5c302016-11-13 14:25:53 -0700907 for fname in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700908 if outcome.config:
Simon Glassc78ed662019-10-31 07:42:53 -0600909 for key, value in outcome.config[fname].items():
Simon Glasscad8abf2015-08-25 21:52:14 -0600910 tconfig.Add(fname, key, value)
Simon Glass8132f982022-07-11 19:03:57 -0600911 config[brd.target] = tconfig
Simon Glassdb17fb82015-02-05 22:06:15 -0700912
Simon Glass8132f982022-07-11 19:03:57 -0600913 tenvironment = Environment(brd.target)
Alex Kiernan4059e302018-05-31 04:48:34 +0000914 if outcome.environment:
Simon Glassc78ed662019-10-31 07:42:53 -0600915 for key, value in outcome.environment.items():
Alex Kiernan4059e302018-05-31 04:48:34 +0000916 tenvironment.Add(key, value)
Simon Glass8132f982022-07-11 19:03:57 -0600917 environment[brd.target] = tenvironment
Alex Kiernan4059e302018-05-31 04:48:34 +0000918
Simon Glass03749d42014-08-28 09:43:44 -0600919 return (board_dict, err_lines_summary, err_lines_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +0000920 warn_lines_summary, warn_lines_boards, config, environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000921
922 def AddOutcome(self, board_dict, arch_list, changes, char, color):
923 """Add an output to our list of outcomes for each architecture
924
925 This simple function adds failing boards (changes) to the
926 relevant architecture string, so we can print the results out
927 sorted by architecture.
928
929 Args:
930 board_dict: Dict containing all boards
931 arch_list: Dict keyed by arch name. Value is a string containing
932 a list of board names which failed for that arch.
933 changes: List of boards to add to arch_list
934 color: terminal.Colour object
935 """
936 done_arch = {}
937 for target in changes:
938 if target in board_dict:
939 arch = board_dict[target].arch
940 else:
941 arch = 'unknown'
Simon Glassf45d3742022-01-29 14:14:17 -0700942 str = self.col.build(color, ' ' + target)
Simon Glassc05694f2013-04-03 11:07:16 +0000943 if not arch in done_arch:
Simon Glassf45d3742022-01-29 14:14:17 -0700944 str = ' %s %s' % (self.col.build(color, char), str)
Simon Glassc05694f2013-04-03 11:07:16 +0000945 done_arch[arch] = True
946 if not arch in arch_list:
947 arch_list[arch] = str
948 else:
949 arch_list[arch] += str
950
951
952 def ColourNum(self, num):
953 color = self.col.RED if num > 0 else self.col.GREEN
954 if num == 0:
955 return '0'
Simon Glassf45d3742022-01-29 14:14:17 -0700956 return self.col.build(color, str(num))
Simon Glassc05694f2013-04-03 11:07:16 +0000957
958 def ResetResultSummary(self, board_selected):
959 """Reset the results summary ready for use.
960
961 Set up the base board list to be all those selected, and set the
962 error lines to empty.
963
964 Following this, calls to PrintResultSummary() will use this
965 information to work out what has changed.
966
967 Args:
968 board_selected: Dict containing boards to summarise, keyed by
969 board.target
970 """
971 self._base_board_dict = {}
Simon Glass8132f982022-07-11 19:03:57 -0600972 for brd in board_selected:
973 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000974 self._base_err_lines = []
Simon Glass03749d42014-08-28 09:43:44 -0600975 self._base_warn_lines = []
976 self._base_err_line_boards = {}
977 self._base_warn_line_boards = {}
Simon Glasscad8abf2015-08-25 21:52:14 -0600978 self._base_config = None
Alex Kiernan4059e302018-05-31 04:48:34 +0000979 self._base_environment = None
Simon Glassc05694f2013-04-03 11:07:16 +0000980
981 def PrintFuncSizeDetail(self, fname, old, new):
982 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
983 delta, common = [], {}
984
985 for a in old:
986 if a in new:
987 common[a] = 1
988
989 for name in old:
990 if name not in common:
991 remove += 1
992 down += old[name]
993 delta.append([-old[name], name])
994
995 for name in new:
996 if name not in common:
997 add += 1
998 up += new[name]
999 delta.append([new[name], name])
1000
1001 for name in common:
1002 diff = new.get(name, 0) - old.get(name, 0)
1003 if diff > 0:
1004 grow, up = grow + 1, up + diff
1005 elif diff < 0:
1006 shrink, down = shrink + 1, down - diff
1007 delta.append([diff, name])
1008
1009 delta.sort()
1010 delta.reverse()
1011
1012 args = [add, -remove, grow, -shrink, up, -down, up - down]
Tom Rini0b48cd62017-05-22 13:48:52 -04001013 if max(args) == 0 and min(args) == 0:
Simon Glassc05694f2013-04-03 11:07:16 +00001014 return
1015 args = [self.ColourNum(x) for x in args]
1016 indent = ' ' * 15
Simon Glass02811582022-01-29 14:14:18 -07001017 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
Simon Glassf45d3742022-01-29 14:14:17 -07001018 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
Simon Glass02811582022-01-29 14:14:18 -07001019 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
Simon Glass4433aa92014-09-05 19:00:07 -06001020 'delta'))
Simon Glassc05694f2013-04-03 11:07:16 +00001021 for diff, name in delta:
1022 if diff:
1023 color = self.col.RED if diff > 0 else self.col.GREEN
1024 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1025 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass02811582022-01-29 14:14:18 -07001026 tprint(msg, colour=color)
Simon Glassc05694f2013-04-03 11:07:16 +00001027
1028
1029 def PrintSizeDetail(self, target_list, show_bloat):
1030 """Show details size information for each board
1031
1032 Args:
1033 target_list: List of targets, each a dict containing:
1034 'target': Target name
1035 'total_diff': Total difference in bytes across all areas
1036 <part_name>: Difference for that part
1037 show_bloat: Show detail for each function
1038 """
1039 targets_by_diff = sorted(target_list, reverse=True,
1040 key=lambda x: x['_total_diff'])
1041 for result in targets_by_diff:
1042 printed_target = False
1043 for name in sorted(result):
1044 diff = result[name]
1045 if name.startswith('_'):
1046 continue
1047 if diff != 0:
1048 color = self.col.RED if diff > 0 else self.col.GREEN
1049 msg = ' %s %+d' % (name, diff)
1050 if not printed_target:
Simon Glass02811582022-01-29 14:14:18 -07001051 tprint('%10s %-15s:' % ('', result['_target']),
Simon Glass4433aa92014-09-05 19:00:07 -06001052 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001053 printed_target = True
Simon Glass02811582022-01-29 14:14:18 -07001054 tprint(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001055 if printed_target:
Simon Glass02811582022-01-29 14:14:18 -07001056 tprint()
Simon Glassc05694f2013-04-03 11:07:16 +00001057 if show_bloat:
1058 target = result['_target']
1059 outcome = result['_outcome']
1060 base_outcome = self._base_board_dict[target]
1061 for fname in outcome.func_sizes:
1062 self.PrintFuncSizeDetail(fname,
1063 base_outcome.func_sizes[fname],
1064 outcome.func_sizes[fname])
1065
1066
1067 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1068 show_bloat):
1069 """Print a summary of image sizes broken down by section.
1070
1071 The summary takes the form of one line per architecture. The
1072 line contains deltas for each of the sections (+ means the section
Flavio Suligoie5e3c112020-01-29 09:56:05 +01001073 got bigger, - means smaller). The numbers are the average number
Simon Glassc05694f2013-04-03 11:07:16 +00001074 of bytes that a board in this section increased by.
1075
1076 For example:
1077 powerpc: (622 boards) text -0.0
1078 arm: (285 boards) text -0.0
Simon Glassc05694f2013-04-03 11:07:16 +00001079
1080 Args:
1081 board_selected: Dict containing boards to summarise, keyed by
1082 board.target
1083 board_dict: Dict containing boards for which we built this
1084 commit, keyed by board.target. The value is an Outcome object.
Simon Glassb4002462020-03-18 09:42:43 -06001085 show_detail: Show size delta detail for each board
Simon Glassc05694f2013-04-03 11:07:16 +00001086 show_bloat: Show detail for each function
1087 """
1088 arch_list = {}
1089 arch_count = {}
1090
1091 # Calculate changes in size for different image parts
1092 # The previous sizes are in Board.sizes, for each board
1093 for target in board_dict:
1094 if target not in board_selected:
1095 continue
1096 base_sizes = self._base_board_dict[target].sizes
1097 outcome = board_dict[target]
1098 sizes = outcome.sizes
1099
1100 # Loop through the list of images, creating a dict of size
1101 # changes for each image/part. We end up with something like
1102 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1103 # which means that U-Boot data increased by 5 bytes and SPL
1104 # text decreased by 4.
1105 err = {'_target' : target}
1106 for image in sizes:
1107 if image in base_sizes:
1108 base_image = base_sizes[image]
1109 # Loop through the text, data, bss parts
1110 for part in sorted(sizes[image]):
1111 diff = sizes[image][part] - base_image[part]
1112 col = None
1113 if diff:
1114 if image == 'u-boot':
1115 name = part
1116 else:
1117 name = image + ':' + part
1118 err[name] = diff
1119 arch = board_selected[target].arch
1120 if not arch in arch_count:
1121 arch_count[arch] = 1
1122 else:
1123 arch_count[arch] += 1
1124 if not sizes:
1125 pass # Only add to our list when we have some stats
1126 elif not arch in arch_list:
1127 arch_list[arch] = [err]
1128 else:
1129 arch_list[arch].append(err)
1130
1131 # We now have a list of image size changes sorted by arch
1132 # Print out a summary of these
Simon Glassc78ed662019-10-31 07:42:53 -06001133 for arch, target_list in arch_list.items():
Simon Glassc05694f2013-04-03 11:07:16 +00001134 # Get total difference for each type
1135 totals = {}
1136 for result in target_list:
1137 total = 0
Simon Glassc78ed662019-10-31 07:42:53 -06001138 for name, diff in result.items():
Simon Glassc05694f2013-04-03 11:07:16 +00001139 if name.startswith('_'):
1140 continue
1141 total += diff
1142 if name in totals:
1143 totals[name] += diff
1144 else:
1145 totals[name] = diff
1146 result['_total_diff'] = total
1147 result['_outcome'] = board_dict[result['_target']]
1148
1149 count = len(target_list)
1150 printed_arch = False
1151 for name in sorted(totals):
1152 diff = totals[name]
1153 if diff:
1154 # Display the average difference in this name for this
1155 # architecture
1156 avg_diff = float(diff) / count
1157 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1158 msg = ' %s %+1.1f' % (name, avg_diff)
1159 if not printed_arch:
Simon Glass02811582022-01-29 14:14:18 -07001160 tprint('%10s: (for %d/%d boards)' % (arch, count,
Simon Glass4433aa92014-09-05 19:00:07 -06001161 arch_count[arch]), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001162 printed_arch = True
Simon Glass02811582022-01-29 14:14:18 -07001163 tprint(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001164
1165 if printed_arch:
Simon Glass02811582022-01-29 14:14:18 -07001166 tprint()
Simon Glassc05694f2013-04-03 11:07:16 +00001167 if show_detail:
1168 self.PrintSizeDetail(target_list, show_bloat)
1169
1170
1171 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glass03749d42014-08-28 09:43:44 -06001172 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001173 config, environment, show_sizes, show_detail,
1174 show_bloat, show_config, show_environment):
Simon Glassc05694f2013-04-03 11:07:16 +00001175 """Compare results with the base results and display delta.
1176
1177 Only boards mentioned in board_selected will be considered. This
1178 function is intended to be called repeatedly with the results of
1179 each commit. It therefore shows a 'diff' between what it saw in
1180 the last call and what it sees now.
1181
1182 Args:
1183 board_selected: Dict containing boards to summarise, keyed by
1184 board.target
1185 board_dict: Dict containing boards for which we built this
1186 commit, keyed by board.target. The value is an Outcome object.
1187 err_lines: A list of errors for this commit, or [] if there is
1188 none, or we don't want to print errors
Simon Glass3394c9f2014-08-28 09:43:43 -06001189 err_line_boards: Dict keyed by error line, containing a list of
1190 the Board objects with that error
Simon Glass03749d42014-08-28 09:43:44 -06001191 warn_lines: A list of warnings for this commit, or [] if there is
1192 none, or we don't want to print errors
1193 warn_line_boards: Dict keyed by warning line, containing a list of
1194 the Board objects with that warning
Simon Glassdb17fb82015-02-05 22:06:15 -07001195 config: Dictionary keyed by filename - e.g. '.config'. Each
1196 value is itself a dictionary:
1197 key: config name
1198 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +00001199 environment: Dictionary keyed by environment variable, Each
1200 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +00001201 show_sizes: Show image size deltas
Simon Glassb4002462020-03-18 09:42:43 -06001202 show_detail: Show size delta detail for each board if show_sizes
Simon Glassc05694f2013-04-03 11:07:16 +00001203 show_bloat: Show detail for each function
Simon Glassdb17fb82015-02-05 22:06:15 -07001204 show_config: Show config changes
Alex Kiernan4059e302018-05-31 04:48:34 +00001205 show_environment: Show environment changes
Simon Glassc05694f2013-04-03 11:07:16 +00001206 """
Simon Glass03749d42014-08-28 09:43:44 -06001207 def _BoardList(line, line_boards):
Simon Glass3394c9f2014-08-28 09:43:43 -06001208 """Helper function to get a line of boards containing a line
1209
1210 Args:
1211 line: Error line to search for
Simon Glassde0fefc2020-04-09 15:08:36 -06001212 line_boards: boards to search, each a Board
Simon Glass3394c9f2014-08-28 09:43:43 -06001213 Return:
Simon Glassde0fefc2020-04-09 15:08:36 -06001214 List of boards with that error line, or [] if the user has not
1215 requested such a list
Simon Glass3394c9f2014-08-28 09:43:43 -06001216 """
Simon Glass5df45222022-07-11 19:04:00 -06001217 brds = []
Simon Glassde0fefc2020-04-09 15:08:36 -06001218 board_set = set()
Simon Glass3394c9f2014-08-28 09:43:43 -06001219 if self._list_error_boards:
Simon Glass8132f982022-07-11 19:03:57 -06001220 for brd in line_boards[line]:
1221 if not brd in board_set:
Simon Glass5df45222022-07-11 19:04:00 -06001222 brds.append(brd)
Simon Glass8132f982022-07-11 19:03:57 -06001223 board_set.add(brd)
Simon Glass5df45222022-07-11 19:04:00 -06001224 return brds
Simon Glass3394c9f2014-08-28 09:43:43 -06001225
Simon Glass03749d42014-08-28 09:43:44 -06001226 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1227 char):
Simon Glassde0fefc2020-04-09 15:08:36 -06001228 """Calculate the required output based on changes in errors
1229
1230 Args:
1231 base_lines: List of errors/warnings for previous commit
1232 base_line_boards: Dict keyed by error line, containing a list
1233 of the Board objects with that error in the previous commit
1234 lines: List of errors/warning for this commit, each a str
1235 line_boards: Dict keyed by error line, containing a list
1236 of the Board objects with that error in this commit
1237 char: Character representing error ('') or warning ('w'). The
1238 broken ('+') or fixed ('-') characters are added in this
1239 function
1240
1241 Returns:
1242 Tuple
1243 List of ErrLine objects for 'better' lines
1244 List of ErrLine objects for 'worse' lines
1245 """
Simon Glass03749d42014-08-28 09:43:44 -06001246 better_lines = []
1247 worse_lines = []
1248 for line in lines:
1249 if line not in base_lines:
Simon Glassde0fefc2020-04-09 15:08:36 -06001250 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1251 line)
1252 worse_lines.append(errline)
Simon Glass03749d42014-08-28 09:43:44 -06001253 for line in base_lines:
1254 if line not in lines:
Simon Glassde0fefc2020-04-09 15:08:36 -06001255 errline = ErrLine(char + '-',
1256 _BoardList(line, base_line_boards), line)
1257 better_lines.append(errline)
Simon Glass03749d42014-08-28 09:43:44 -06001258 return better_lines, worse_lines
1259
Simon Glassdb17fb82015-02-05 22:06:15 -07001260 def _CalcConfig(delta, name, config):
1261 """Calculate configuration changes
1262
1263 Args:
1264 delta: Type of the delta, e.g. '+'
1265 name: name of the file which changed (e.g. .config)
1266 config: configuration change dictionary
1267 key: config name
1268 value: config value
1269 Returns:
1270 String containing the configuration changes which can be
1271 printed
1272 """
1273 out = ''
1274 for key in sorted(config.keys()):
1275 out += '%s=%s ' % (key, config[key])
Simon Glasscad8abf2015-08-25 21:52:14 -06001276 return '%s %s: %s' % (delta, name, out)
Simon Glassdb17fb82015-02-05 22:06:15 -07001277
Simon Glasscad8abf2015-08-25 21:52:14 -06001278 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1279 """Add changes in configuration to a list
Simon Glassdb17fb82015-02-05 22:06:15 -07001280
1281 Args:
Simon Glasscad8abf2015-08-25 21:52:14 -06001282 lines: list to add to
1283 name: config file name
Simon Glassdb17fb82015-02-05 22:06:15 -07001284 config_plus: configurations added, dictionary
1285 key: config name
1286 value: config value
1287 config_minus: configurations removed, dictionary
1288 key: config name
1289 value: config value
1290 config_change: configurations changed, dictionary
1291 key: config name
1292 value: config value
1293 """
1294 if config_plus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001295 lines.append(_CalcConfig('+', name, config_plus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001296 if config_minus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001297 lines.append(_CalcConfig('-', name, config_minus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001298 if config_change:
Simon Glasscad8abf2015-08-25 21:52:14 -06001299 lines.append(_CalcConfig('c', name, config_change))
1300
1301 def _OutputConfigInfo(lines):
1302 for line in lines:
1303 if not line:
1304 continue
1305 if line[0] == '+':
1306 col = self.col.GREEN
1307 elif line[0] == '-':
1308 col = self.col.RED
1309 elif line[0] == 'c':
1310 col = self.col.YELLOW
Simon Glass02811582022-01-29 14:14:18 -07001311 tprint(' ' + line, newline=True, colour=col)
Simon Glasscad8abf2015-08-25 21:52:14 -06001312
Simon Glassac500222020-04-09 15:08:28 -06001313 def _OutputErrLines(err_lines, colour):
1314 """Output the line of error/warning lines, if not empty
1315
1316 Also increments self._error_lines if err_lines not empty
1317
1318 Args:
Simon Glassde0fefc2020-04-09 15:08:36 -06001319 err_lines: List of ErrLine objects, each an error or warning
1320 line, possibly including a list of boards with that
1321 error/warning
Simon Glassac500222020-04-09 15:08:28 -06001322 colour: Colour to use for output
1323 """
1324 if err_lines:
Simon Glassea49f9b2020-04-09 15:08:37 -06001325 out_list = []
Simon Glassde0fefc2020-04-09 15:08:36 -06001326 for line in err_lines:
Simon Glass5df45222022-07-11 19:04:00 -06001327 names = [brd.target for brd in line.brds]
Simon Glass070589b2020-04-09 15:08:38 -06001328 board_str = ' '.join(names) if names else ''
Simon Glassea49f9b2020-04-09 15:08:37 -06001329 if board_str:
Simon Glassf45d3742022-01-29 14:14:17 -07001330 out = self.col.build(colour, line.char + '(')
1331 out += self.col.build(self.col.MAGENTA, board_str,
Simon Glassea49f9b2020-04-09 15:08:37 -06001332 bright=False)
Simon Glassf45d3742022-01-29 14:14:17 -07001333 out += self.col.build(colour, ') %s' % line.errline)
Simon Glassea49f9b2020-04-09 15:08:37 -06001334 else:
Simon Glassf45d3742022-01-29 14:14:17 -07001335 out = self.col.build(colour, line.char + line.errline)
Simon Glassea49f9b2020-04-09 15:08:37 -06001336 out_list.append(out)
Simon Glass02811582022-01-29 14:14:18 -07001337 tprint('\n'.join(out_list))
Simon Glassac500222020-04-09 15:08:28 -06001338 self._error_lines += 1
1339
Simon Glassdb17fb82015-02-05 22:06:15 -07001340
Simon Glass454507f2018-11-06 16:02:12 -07001341 ok_boards = [] # List of boards fixed since last commit
Simon Glass071a1782018-11-06 16:02:13 -07001342 warn_boards = [] # List of boards with warnings since last commit
Simon Glass454507f2018-11-06 16:02:12 -07001343 err_boards = [] # List of new broken boards since last commit
1344 new_boards = [] # List of boards that didn't exist last time
1345 unknown_boards = [] # List of boards that were not built
Simon Glassc05694f2013-04-03 11:07:16 +00001346
1347 for target in board_dict:
1348 if target not in board_selected:
1349 continue
1350
1351 # If the board was built last time, add its outcome to a list
1352 if target in self._base_board_dict:
1353 base_outcome = self._base_board_dict[target].rc
1354 outcome = board_dict[target]
1355 if outcome.rc == OUTCOME_UNKNOWN:
Simon Glass454507f2018-11-06 16:02:12 -07001356 unknown_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001357 elif outcome.rc < base_outcome:
Simon Glass071a1782018-11-06 16:02:13 -07001358 if outcome.rc == OUTCOME_WARNING:
1359 warn_boards.append(target)
1360 else:
1361 ok_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001362 elif outcome.rc > base_outcome:
Simon Glass071a1782018-11-06 16:02:13 -07001363 if outcome.rc == OUTCOME_WARNING:
1364 warn_boards.append(target)
1365 else:
1366 err_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001367 else:
Simon Glass454507f2018-11-06 16:02:12 -07001368 new_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001369
Simon Glassac500222020-04-09 15:08:28 -06001370 # Get a list of errors and warnings that have appeared, and disappeared
Simon Glass03749d42014-08-28 09:43:44 -06001371 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1372 self._base_err_line_boards, err_lines, err_line_boards, '')
1373 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1374 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassc05694f2013-04-03 11:07:16 +00001375
Simon Glass6c435622022-07-11 19:03:56 -06001376 # For the IDE mode, print out all the output
1377 if self._ide:
1378 outcome = board_dict[target]
1379 for line in outcome.err_lines:
1380 sys.stderr.write(line)
1381
Simon Glassc05694f2013-04-03 11:07:16 +00001382 # Display results by arch
Simon Glass6c435622022-07-11 19:03:56 -06001383 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
Simon Glass071a1782018-11-06 16:02:13 -07001384 worse_err, better_err, worse_warn, better_warn)):
Simon Glassc05694f2013-04-03 11:07:16 +00001385 arch_list = {}
Simon Glass454507f2018-11-06 16:02:12 -07001386 self.AddOutcome(board_selected, arch_list, ok_boards, '',
Simon Glassc05694f2013-04-03 11:07:16 +00001387 self.col.GREEN)
Simon Glass071a1782018-11-06 16:02:13 -07001388 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1389 self.col.YELLOW)
Simon Glass454507f2018-11-06 16:02:12 -07001390 self.AddOutcome(board_selected, arch_list, err_boards, '+',
Simon Glassc05694f2013-04-03 11:07:16 +00001391 self.col.RED)
Simon Glass454507f2018-11-06 16:02:12 -07001392 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
Simon Glassc05694f2013-04-03 11:07:16 +00001393 if self._show_unknown:
Simon Glass454507f2018-11-06 16:02:12 -07001394 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
Simon Glassc05694f2013-04-03 11:07:16 +00001395 self.col.MAGENTA)
Simon Glassc78ed662019-10-31 07:42:53 -06001396 for arch, target_list in arch_list.items():
Simon Glass02811582022-01-29 14:14:18 -07001397 tprint('%10s: %s' % (arch, target_list))
Simon Glassbb4dffb2014-08-09 15:33:06 -06001398 self._error_lines += 1
Simon Glassac500222020-04-09 15:08:28 -06001399 _OutputErrLines(better_err, colour=self.col.GREEN)
1400 _OutputErrLines(worse_err, colour=self.col.RED)
1401 _OutputErrLines(better_warn, colour=self.col.CYAN)
Simon Glass564ddac2020-04-09 15:08:35 -06001402 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
Simon Glassc05694f2013-04-03 11:07:16 +00001403
1404 if show_sizes:
1405 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1406 show_bloat)
1407
Alex Kiernan4059e302018-05-31 04:48:34 +00001408 if show_environment and self._base_environment:
1409 lines = []
1410
1411 for target in board_dict:
1412 if target not in board_selected:
1413 continue
1414
1415 tbase = self._base_environment[target]
1416 tenvironment = environment[target]
1417 environment_plus = {}
1418 environment_minus = {}
1419 environment_change = {}
1420 base = tbase.environment
Simon Glassc78ed662019-10-31 07:42:53 -06001421 for key, value in tenvironment.environment.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001422 if key not in base:
1423 environment_plus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001424 for key, value in base.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001425 if key not in tenvironment.environment:
1426 environment_minus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001427 for key, value in base.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001428 new_value = tenvironment.environment.get(key)
1429 if new_value and value != new_value:
1430 desc = '%s -> %s' % (value, new_value)
1431 environment_change[key] = desc
1432
1433 _AddConfig(lines, target, environment_plus, environment_minus,
1434 environment_change)
1435
1436 _OutputConfigInfo(lines)
1437
Simon Glasscad8abf2015-08-25 21:52:14 -06001438 if show_config and self._base_config:
1439 summary = {}
1440 arch_config_plus = {}
1441 arch_config_minus = {}
1442 arch_config_change = {}
1443 arch_list = []
1444
1445 for target in board_dict:
1446 if target not in board_selected:
1447 continue
1448 arch = board_selected[target].arch
1449 if arch not in arch_list:
1450 arch_list.append(arch)
1451
1452 for arch in arch_list:
1453 arch_config_plus[arch] = {}
1454 arch_config_minus[arch] = {}
1455 arch_config_change[arch] = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001456 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001457 arch_config_plus[arch][name] = {}
1458 arch_config_minus[arch][name] = {}
1459 arch_config_change[arch][name] = {}
1460
1461 for target in board_dict:
1462 if target not in board_selected:
Simon Glassdb17fb82015-02-05 22:06:15 -07001463 continue
Simon Glasscad8abf2015-08-25 21:52:14 -06001464
1465 arch = board_selected[target].arch
1466
1467 all_config_plus = {}
1468 all_config_minus = {}
1469 all_config_change = {}
1470 tbase = self._base_config[target]
1471 tconfig = config[target]
1472 lines = []
Simon Glasscde5c302016-11-13 14:25:53 -07001473 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001474 if not tconfig.config[name]:
1475 continue
1476 config_plus = {}
1477 config_minus = {}
1478 config_change = {}
1479 base = tbase.config[name]
Simon Glassc78ed662019-10-31 07:42:53 -06001480 for key, value in tconfig.config[name].items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001481 if key not in base:
1482 config_plus[key] = value
1483 all_config_plus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001484 for key, value in base.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001485 if key not in tconfig.config[name]:
1486 config_minus[key] = value
1487 all_config_minus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001488 for key, value in base.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001489 new_value = tconfig.config.get(key)
1490 if new_value and value != new_value:
1491 desc = '%s -> %s' % (value, new_value)
1492 config_change[key] = desc
1493 all_config_change[key] = desc
1494
1495 arch_config_plus[arch][name].update(config_plus)
1496 arch_config_minus[arch][name].update(config_minus)
1497 arch_config_change[arch][name].update(config_change)
1498
1499 _AddConfig(lines, name, config_plus, config_minus,
1500 config_change)
1501 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1502 all_config_change)
1503 summary[target] = '\n'.join(lines)
1504
1505 lines_by_target = {}
Simon Glassc78ed662019-10-31 07:42:53 -06001506 for target, lines in summary.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001507 if lines in lines_by_target:
1508 lines_by_target[lines].append(target)
1509 else:
1510 lines_by_target[lines] = [target]
1511
1512 for arch in arch_list:
1513 lines = []
1514 all_plus = {}
1515 all_minus = {}
1516 all_change = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001517 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001518 all_plus.update(arch_config_plus[arch][name])
1519 all_minus.update(arch_config_minus[arch][name])
1520 all_change.update(arch_config_change[arch][name])
1521 _AddConfig(lines, name, arch_config_plus[arch][name],
1522 arch_config_minus[arch][name],
1523 arch_config_change[arch][name])
1524 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1525 #arch_summary[target] = '\n'.join(lines)
1526 if lines:
Simon Glass02811582022-01-29 14:14:18 -07001527 tprint('%s:' % arch)
Simon Glasscad8abf2015-08-25 21:52:14 -06001528 _OutputConfigInfo(lines)
1529
Simon Glassc78ed662019-10-31 07:42:53 -06001530 for lines, targets in lines_by_target.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001531 if not lines:
1532 continue
Simon Glass02811582022-01-29 14:14:18 -07001533 tprint('%s :' % ' '.join(sorted(targets)))
Simon Glasscad8abf2015-08-25 21:52:14 -06001534 _OutputConfigInfo(lines.split('\n'))
1535
Simon Glassdb17fb82015-02-05 22:06:15 -07001536
Simon Glassc05694f2013-04-03 11:07:16 +00001537 # Save our updated information for the next call to this function
1538 self._base_board_dict = board_dict
1539 self._base_err_lines = err_lines
Simon Glass03749d42014-08-28 09:43:44 -06001540 self._base_warn_lines = warn_lines
1541 self._base_err_line_boards = err_line_boards
1542 self._base_warn_line_boards = warn_line_boards
Simon Glassdb17fb82015-02-05 22:06:15 -07001543 self._base_config = config
Alex Kiernan4059e302018-05-31 04:48:34 +00001544 self._base_environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +00001545
1546 # Get a list of boards that did not get built, if needed
1547 not_built = []
Simon Glass8132f982022-07-11 19:03:57 -06001548 for brd in board_selected:
1549 if not brd in board_dict:
1550 not_built.append(brd)
Simon Glassc05694f2013-04-03 11:07:16 +00001551 if not_built:
Simon Glass02811582022-01-29 14:14:18 -07001552 tprint("Boards not built (%d): %s" % (len(not_built),
Simon Glass4433aa92014-09-05 19:00:07 -06001553 ', '.join(not_built)))
Simon Glassc05694f2013-04-03 11:07:16 +00001554
Simon Glasseb48bbc2014-08-09 15:33:02 -06001555 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glass03749d42014-08-28 09:43:44 -06001556 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan4059e302018-05-31 04:48:34 +00001557 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glass3394c9f2014-08-28 09:43:43 -06001558 board_selected, commit_upto,
Simon Glassdb17fb82015-02-05 22:06:15 -07001559 read_func_sizes=self._show_bloat,
Alex Kiernan4059e302018-05-31 04:48:34 +00001560 read_config=self._show_config,
1561 read_environment=self._show_environment)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001562 if commits:
1563 msg = '%02d: %s' % (commit_upto + 1,
1564 commits[commit_upto].subject)
Simon Glass02811582022-01-29 14:14:18 -07001565 tprint(msg, colour=self.col.BLUE)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001566 self.PrintResultSummary(board_selected, board_dict,
Simon Glass3394c9f2014-08-28 09:43:43 -06001567 err_lines if self._show_errors else [], err_line_boards,
Simon Glass03749d42014-08-28 09:43:44 -06001568 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001569 config, environment, self._show_sizes, self._show_detail,
1570 self._show_bloat, self._show_config, self._show_environment)
Simon Glassc05694f2013-04-03 11:07:16 +00001571
Simon Glasseb48bbc2014-08-09 15:33:02 -06001572 def ShowSummary(self, commits, board_selected):
Simon Glassc05694f2013-04-03 11:07:16 +00001573 """Show a build summary for U-Boot for a given board list.
1574
1575 Reset the result summary, then repeatedly call GetResultSummary on
1576 each commit's results, then display the differences we see.
1577
1578 Args:
1579 commit: Commit objects to summarise
1580 board_selected: Dict containing boards to summarise
Simon Glassc05694f2013-04-03 11:07:16 +00001581 """
Simon Glassd326ad72014-08-09 15:32:59 -06001582 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001583 self.commits = commits
1584 self.ResetResultSummary(board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001585 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +00001586
1587 for commit_upto in range(0, self.commit_count, self._step):
Simon Glasseb48bbc2014-08-09 15:33:02 -06001588 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001589 if not self._error_lines:
Simon Glass02811582022-01-29 14:14:18 -07001590 tprint('(no errors to report)', colour=self.col.GREEN)
Simon Glassc05694f2013-04-03 11:07:16 +00001591
1592
1593 def SetupBuild(self, board_selected, commits):
1594 """Set up ready to start a build.
1595
1596 Args:
1597 board_selected: Selected boards to build
1598 commits: Selected commits to build
1599 """
1600 # First work out how many commits we will build
Simon Glassc78ed662019-10-31 07:42:53 -06001601 count = (self.commit_count + self._step - 1) // self._step
Simon Glassc05694f2013-04-03 11:07:16 +00001602 self.count = len(board_selected) * count
1603 self.upto = self.warned = self.fail = 0
1604 self._timestamps = collections.deque()
1605
Simon Glassc05694f2013-04-03 11:07:16 +00001606 def GetThreadDir(self, thread_num):
1607 """Get the directory path to the working dir for a thread.
1608
1609 Args:
Simon Glassc635d892021-01-30 22:17:46 -07001610 thread_num: Number of thread to check (-1 for main process, which
1611 is treated as 0)
Simon Glassc05694f2013-04-03 11:07:16 +00001612 """
Simon Glassb6eb8cf2020-03-18 09:42:42 -06001613 if self.work_in_output:
1614 return self._working_dir
Simon Glassc635d892021-01-30 22:17:46 -07001615 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
Simon Glassc05694f2013-04-03 11:07:16 +00001616
Simon Glassd326ad72014-08-09 15:32:59 -06001617 def _PrepareThread(self, thread_num, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001618 """Prepare the working directory for a thread.
1619
1620 This clones or fetches the repo into the thread's work directory.
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001621 Optionally, it can create a linked working tree of the repo in the
1622 thread's work directory instead.
Simon Glassc05694f2013-04-03 11:07:16 +00001623
1624 Args:
1625 thread_num: Thread number (0, 1, ...)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001626 setup_git:
1627 'clone' to set up a git clone
1628 'worktree' to set up a git worktree
Simon Glassc05694f2013-04-03 11:07:16 +00001629 """
1630 thread_dir = self.GetThreadDir(thread_num)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001631 builderthread.Mkdir(thread_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001632 git_dir = os.path.join(thread_dir, '.git')
1633
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001634 # Create a worktree or a git repo clone for this thread if it
1635 # doesn't already exist
Simon Glassd326ad72014-08-09 15:32:59 -06001636 if setup_git and self.git_dir:
Simon Glassc05694f2013-04-03 11:07:16 +00001637 src_dir = os.path.abspath(self.git_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001638 if os.path.isdir(git_dir):
1639 # This is a clone of the src_dir repo, we can keep using
1640 # it but need to fetch from src_dir.
Simon Glass02811582022-01-29 14:14:18 -07001641 tprint('\rFetching repo for thread %d' % thread_num,
Simon Glass43054932020-04-09 15:08:43 -06001642 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001643 gitutil.fetch(git_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001644 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001645 elif os.path.isfile(git_dir):
1646 # This is a worktree of the src_dir repo, we don't need to
1647 # create it again or update it in any way.
1648 pass
1649 elif os.path.exists(git_dir):
1650 # Don't know what could trigger this, but we probably
1651 # can't create a git worktree/clone here.
1652 raise ValueError('Git dir %s exists, but is not a file '
1653 'or a directory.' % git_dir)
1654 elif setup_git == 'worktree':
Simon Glass02811582022-01-29 14:14:18 -07001655 tprint('\rChecking out worktree for thread %d' % thread_num,
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001656 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001657 gitutil.add_worktree(src_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001658 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001659 elif setup_git == 'clone' or setup_git == True:
Simon Glass02811582022-01-29 14:14:18 -07001660 tprint('\rCloning repo for thread %d' % thread_num,
Simon Glass2e2a6e62016-09-18 16:48:31 -06001661 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001662 gitutil.clone(src_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001663 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001664 else:
1665 raise ValueError("Can't setup git repo with %s." % setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001666
Simon Glassd326ad72014-08-09 15:32:59 -06001667 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001668 """Prepare the working directory for use.
1669
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001670 Set up the git repo for each thread. Creates a linked working tree
1671 if git-worktree is available, or clones the repo if it isn't.
Simon Glassc05694f2013-04-03 11:07:16 +00001672
1673 Args:
Simon Glassc635d892021-01-30 22:17:46 -07001674 max_threads: Maximum number of threads we expect to need. If 0 then
1675 1 is set up, since the main process still needs somewhere to
1676 work
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001677 setup_git: True to set up a git worktree or a git clone
Simon Glassc05694f2013-04-03 11:07:16 +00001678 """
Simon Glass4a1e88b2014-08-09 15:33:00 -06001679 builderthread.Mkdir(self._working_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001680 if setup_git and self.git_dir:
1681 src_dir = os.path.abspath(self.git_dir)
Simon Glass761648b2022-01-29 14:14:11 -07001682 if gitutil.check_worktree_is_available(src_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001683 setup_git = 'worktree'
1684 # If we previously added a worktree but the directory for it
1685 # got deleted, we need to prune its files from the repo so
1686 # that we can check out another in its place.
Simon Glass761648b2022-01-29 14:14:11 -07001687 gitutil.prune_worktrees(src_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001688 else:
1689 setup_git = 'clone'
Simon Glassc635d892021-01-30 22:17:46 -07001690
1691 # Always do at least one thread
1692 for thread in range(max(max_threads, 1)):
Simon Glassd326ad72014-08-09 15:32:59 -06001693 self._PrepareThread(thread, setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001694
Simon Glass5dc1ca72020-03-18 09:42:45 -06001695 def _GetOutputSpaceRemovals(self):
Simon Glassc05694f2013-04-03 11:07:16 +00001696 """Get the output directories ready to receive files.
1697
Simon Glass5dc1ca72020-03-18 09:42:45 -06001698 Figure out what needs to be deleted in the output directory before it
1699 can be used. We only delete old buildman directories which have the
1700 expected name pattern. See _GetOutputDir().
1701
1702 Returns:
1703 List of full paths of directories to remove
Simon Glassc05694f2013-04-03 11:07:16 +00001704 """
Simon Glassb9a9b7c2014-12-01 17:33:53 -07001705 if not self.commits:
1706 return
Simon Glassc05694f2013-04-03 11:07:16 +00001707 dir_list = []
1708 for commit_upto in range(self.commit_count):
1709 dir_list.append(self._GetOutputDir(commit_upto))
1710
Simon Glass83cb6cc2016-09-18 16:48:32 -06001711 to_remove = []
Simon Glassc05694f2013-04-03 11:07:16 +00001712 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1713 if dirname not in dir_list:
Simon Glass5dc1ca72020-03-18 09:42:45 -06001714 leaf = dirname[len(self.base_dir) + 1:]
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +03001715 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
Simon Glass5dc1ca72020-03-18 09:42:45 -06001716 if m:
1717 to_remove.append(dirname)
1718 return to_remove
1719
1720 def _PrepareOutputSpace(self):
1721 """Get the output directories ready to receive files.
1722
1723 We delete any output directories which look like ones we need to
1724 create. Having left over directories is confusing when the user wants
1725 to check the output manually.
1726 """
1727 to_remove = self._GetOutputSpaceRemovals()
Simon Glass83cb6cc2016-09-18 16:48:32 -06001728 if to_remove:
Simon Glass02811582022-01-29 14:14:18 -07001729 tprint('Removing %d old build directories...' % len(to_remove),
Simon Glass83cb6cc2016-09-18 16:48:32 -06001730 newline=False)
1731 for dirname in to_remove:
Simon Glassc05694f2013-04-03 11:07:16 +00001732 shutil.rmtree(dirname)
Simon Glass02811582022-01-29 14:14:18 -07001733 terminal.print_clear()
Simon Glassc05694f2013-04-03 11:07:16 +00001734
Simon Glass78e418e2014-08-09 15:33:03 -06001735 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +00001736 """Build all commits for a list of boards
1737
1738 Args:
1739 commits: List of commits to be build, each a Commit object
1740 boards_selected: Dict of selected boards, key is target name,
1741 value is Board object
Simon Glassc05694f2013-04-03 11:07:16 +00001742 keep_outputs: True to save build output files
Simon Glass78e418e2014-08-09 15:33:03 -06001743 verbose: Display build results as they are completed
Simon Glassc2f91072014-08-28 09:43:39 -06001744 Returns:
1745 Tuple containing:
1746 - number of boards that failed to build
1747 - number of boards that issued warnings
Simon Glass9bf9a722021-04-11 16:27:27 +12001748 - list of thread exceptions raised
Simon Glassc05694f2013-04-03 11:07:16 +00001749 """
Simon Glassd326ad72014-08-09 15:32:59 -06001750 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001751 self.commits = commits
Simon Glass78e418e2014-08-09 15:33:03 -06001752 self._verbose = verbose
Simon Glassc05694f2013-04-03 11:07:16 +00001753
1754 self.ResetResultSummary(board_selected)
Thierry Reding336d5bd2014-08-19 10:22:39 +02001755 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassd326ad72014-08-09 15:32:59 -06001756 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1757 commits is not None)
Simon Glassc05694f2013-04-03 11:07:16 +00001758 self._PrepareOutputSpace()
Simon Glass6c435622022-07-11 19:03:56 -06001759 if not self._ide:
1760 tprint('\rStarting build...', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001761 self.SetupBuild(board_selected, commits)
1762 self.ProcessResult(None)
Simon Glass9bf9a722021-04-11 16:27:27 +12001763 self.thread_exceptions = []
Simon Glassc05694f2013-04-03 11:07:16 +00001764 # Create jobs to build all commits for each board
Simon Glassc78ed662019-10-31 07:42:53 -06001765 for brd in board_selected.values():
Simon Glass4a1e88b2014-08-09 15:33:00 -06001766 job = builderthread.BuilderJob()
Simon Glass8132f982022-07-11 19:03:57 -06001767 job.brd = brd
Simon Glassc05694f2013-04-03 11:07:16 +00001768 job.commits = commits
1769 job.keep_outputs = keep_outputs
Simon Glassb6eb8cf2020-03-18 09:42:42 -06001770 job.work_in_output = self.work_in_output
Simon Glasse5650a82022-01-22 05:07:33 -07001771 job.adjust_cfg = self.adjust_cfg
Simon Glassc05694f2013-04-03 11:07:16 +00001772 job.step = self._step
Simon Glassc635d892021-01-30 22:17:46 -07001773 if self.num_threads:
1774 self.queue.put(job)
1775 else:
Simon Glasse36fe012022-02-11 13:23:19 -07001776 self._single_builder.RunJob(job)
Simon Glassc05694f2013-04-03 11:07:16 +00001777
Simon Glassc635d892021-01-30 22:17:46 -07001778 if self.num_threads:
1779 term = threading.Thread(target=self.queue.join)
1780 term.setDaemon(True)
1781 term.start()
1782 while term.is_alive():
1783 term.join(100)
Simon Glassc05694f2013-04-03 11:07:16 +00001784
Simon Glassc635d892021-01-30 22:17:46 -07001785 # Wait until we have processed all output
1786 self.out_queue.join()
Simon Glass6c435622022-07-11 19:03:56 -06001787 if not self._ide:
1788 tprint()
Simon Glass726ae812020-04-09 15:08:47 -06001789
Simon Glass6c435622022-07-11 19:03:56 -06001790 msg = 'Completed: %d total built' % self.count
1791 if self.already_done:
1792 msg += ' (%d previously' % self.already_done
1793 if self.already_done != self.count:
1794 msg += ', %d newly' % (self.count - self.already_done)
1795 msg += ')'
1796 duration = datetime.now() - self._start_time
1797 if duration > timedelta(microseconds=1000000):
1798 if duration.microseconds >= 500000:
1799 duration = duration + timedelta(seconds=1)
1800 duration = duration - timedelta(microseconds=duration.microseconds)
1801 rate = float(self.count) / duration.total_seconds()
1802 msg += ', duration %s, rate %1.2f' % (duration, rate)
1803 tprint(msg)
1804 if self.thread_exceptions:
1805 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1806 colour=self.col.RED)
Simon Glass726ae812020-04-09 15:08:47 -06001807
Simon Glass9bf9a722021-04-11 16:27:27 +12001808 return (self.fail, self.warned, self.thread_exceptions)