Prepare v2023.10
[platform/kernel/u-boot.git] / test / py / u_boot_console_base.py
1 # SPDX-License-Identifier: GPL-2.0
2 # Copyright (c) 2015 Stephen Warren
3 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
4
5 # Common logic to interact with U-Boot via the console. This class provides
6 # the interface that tests use to execute U-Boot shell commands and wait for
7 # their results. Sub-classes exist to perform board-type-specific setup
8 # operations, such as spawning a sub-process for Sandbox, or attaching to the
9 # serial console of real hardware.
10
11 import multiplexed_log
12 import os
13 import pytest
14 import re
15 import sys
16 import u_boot_spawn
17
18 # Regexes for text we expect U-Boot to send to the console.
19 pattern_u_boot_spl_signon = re.compile('(U-Boot SPL \\d{4}\\.\\d{2}[^\r\n]*\\))')
20 pattern_u_boot_spl2_signon = re.compile('(U-Boot SPL \\d{4}\\.\\d{2}[^\r\n]*\\))')
21 pattern_u_boot_main_signon = re.compile('(U-Boot \\d{4}\\.\\d{2}[^\r\n]*\\))')
22 pattern_stop_autoboot_prompt = re.compile('Hit any key to stop autoboot: ')
23 pattern_unknown_command = re.compile('Unknown command \'.*\' - try \'help\'')
24 pattern_error_notification = re.compile('## Error: ')
25 pattern_error_please_reset = re.compile('### ERROR ### Please RESET the board ###')
26
27 PAT_ID = 0
28 PAT_RE = 1
29
30 bad_pattern_defs = (
31     ('spl_signon', pattern_u_boot_spl_signon),
32     ('spl2_signon', pattern_u_boot_spl2_signon),
33     ('main_signon', pattern_u_boot_main_signon),
34     ('stop_autoboot_prompt', pattern_stop_autoboot_prompt),
35     ('unknown_command', pattern_unknown_command),
36     ('error_notification', pattern_error_notification),
37     ('error_please_reset', pattern_error_please_reset),
38 )
39
40 class ConsoleDisableCheck(object):
41     """Context manager (for Python's with statement) that temporarily disables
42     the specified console output error check. This is useful when deliberately
43     executing a command that is known to trigger one of the error checks, in
44     order to test that the error condition is actually raised. This class is
45     used internally by ConsoleBase::disable_check(); it is not intended for
46     direct usage."""
47
48     def __init__(self, console, check_type):
49         self.console = console
50         self.check_type = check_type
51
52     def __enter__(self):
53         self.console.disable_check_count[self.check_type] += 1
54         self.console.eval_bad_patterns()
55
56     def __exit__(self, extype, value, traceback):
57         self.console.disable_check_count[self.check_type] -= 1
58         self.console.eval_bad_patterns()
59
60 class ConsoleSetupTimeout(object):
61     """Context manager (for Python's with statement) that temporarily sets up
62     timeout for specific command. This is useful when execution time is greater
63     then default 30s."""
64
65     def __init__(self, console, timeout):
66         self.p = console.p
67         self.orig_timeout = self.p.timeout
68         self.p.timeout = timeout
69
70     def __enter__(self):
71         return self
72
73     def __exit__(self, extype, value, traceback):
74         self.p.timeout = self.orig_timeout
75
76 class ConsoleBase(object):
77     """The interface through which test functions interact with the U-Boot
78     console. This primarily involves executing shell commands, capturing their
79     results, and checking for common error conditions. Some common utilities
80     are also provided too."""
81
82     def __init__(self, log, config, max_fifo_fill):
83         """Initialize a U-Boot console connection.
84
85         Can only usefully be called by sub-classes.
86
87         Args:
88             log: A mulptiplex_log.Logfile object, to which the U-Boot output
89                 will be logged.
90             config: A configuration data structure, as built by conftest.py.
91             max_fifo_fill: The maximum number of characters to send to U-Boot
92                 command-line before waiting for U-Boot to echo the characters
93                 back. For UART-based HW without HW flow control, this value
94                 should be set less than the UART RX FIFO size to avoid
95                 overflow, assuming that U-Boot can't keep up with full-rate
96                 traffic at the baud rate.
97
98         Returns:
99             Nothing.
100         """
101
102         self.log = log
103         self.config = config
104         self.max_fifo_fill = max_fifo_fill
105
106         self.logstream = self.log.get_stream('console', sys.stdout)
107
108         # Array slice removes leading/trailing quotes
109         self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
110         self.prompt_compiled = re.compile('^' + re.escape(self.prompt), re.MULTILINE)
111         self.p = None
112         self.disable_check_count = {pat[PAT_ID]: 0 for pat in bad_pattern_defs}
113         self.eval_bad_patterns()
114
115         self.at_prompt = False
116         self.at_prompt_logevt = None
117
118     def get_spawn(self):
119         # This is not called, ssubclass must define this.
120         # Return a value to avoid:
121         #   u_boot_console_base.py:348:12: E1128: Assigning result of a function
122         #   call, where the function returns None (assignment-from-none)
123         return u_boot_spawn.Spawn([])
124
125
126     def eval_bad_patterns(self):
127         self.bad_patterns = [pat[PAT_RE] for pat in bad_pattern_defs \
128             if self.disable_check_count[pat[PAT_ID]] == 0]
129         self.bad_pattern_ids = [pat[PAT_ID] for pat in bad_pattern_defs \
130             if self.disable_check_count[pat[PAT_ID]] == 0]
131
132     def close(self):
133         """Terminate the connection to the U-Boot console.
134
135         This function is only useful once all interaction with U-Boot is
136         complete. Once this function is called, data cannot be sent to or
137         received from U-Boot.
138
139         Args:
140             None.
141
142         Returns:
143             Nothing.
144         """
145
146         if self.p:
147             self.p.close()
148         self.logstream.close()
149
150     def wait_for_boot_prompt(self, loop_num = 1):
151         """Wait for the boot up until command prompt. This is for internal use only.
152         """
153         try:
154             bcfg = self.config.buildconfig
155             config_spl = bcfg.get('config_spl', 'n') == 'y'
156             config_spl_serial = bcfg.get('config_spl_serial', 'n') == 'y'
157             env_spl_skipped = self.config.env.get('env__spl_skipped', False)
158             env_spl2_skipped = self.config.env.get('env__spl2_skipped', True)
159
160             while loop_num > 0:
161                 loop_num -= 1
162                 if config_spl and config_spl_serial and not env_spl_skipped:
163                     m = self.p.expect([pattern_u_boot_spl_signon] +
164                                       self.bad_patterns)
165                     if m != 0:
166                         raise Exception('Bad pattern found on SPL console: ' +
167                                         self.bad_pattern_ids[m - 1])
168                 if not env_spl2_skipped:
169                     m = self.p.expect([pattern_u_boot_spl2_signon] +
170                                       self.bad_patterns)
171                     if m != 0:
172                         raise Exception('Bad pattern found on SPL2 console: ' +
173                                         self.bad_pattern_ids[m - 1])
174                 m = self.p.expect([pattern_u_boot_main_signon] + self.bad_patterns)
175                 if m != 0:
176                     raise Exception('Bad pattern found on console: ' +
177                                     self.bad_pattern_ids[m - 1])
178             self.u_boot_version_string = self.p.after
179             while True:
180                 m = self.p.expect([self.prompt_compiled,
181                     pattern_stop_autoboot_prompt] + self.bad_patterns)
182                 if m == 0:
183                     break
184                 if m == 1:
185                     self.p.send(' ')
186                     continue
187                 raise Exception('Bad pattern found on console: ' +
188                                 self.bad_pattern_ids[m - 2])
189
190         except Exception as ex:
191             self.log.error(str(ex))
192             self.cleanup_spawn()
193             raise
194         finally:
195             self.log.timestamp()
196
197     def run_command(self, cmd, wait_for_echo=True, send_nl=True,
198             wait_for_prompt=True, wait_for_reboot=False):
199         """Execute a command via the U-Boot console.
200
201         The command is always sent to U-Boot.
202
203         U-Boot echoes any command back to its output, and this function
204         typically waits for that to occur. The wait can be disabled by setting
205         wait_for_echo=False, which is useful e.g. when sending CTRL-C to
206         interrupt a long-running command such as "ums".
207
208         Command execution is typically triggered by sending a newline
209         character. This can be disabled by setting send_nl=False, which is
210         also useful when sending CTRL-C.
211
212         This function typically waits for the command to finish executing, and
213         returns the console output that it generated. This can be disabled by
214         setting wait_for_prompt=False, which is useful when invoking a long-
215         running command such as "ums".
216
217         Args:
218             cmd: The command to send.
219             wait_for_echo: Boolean indicating whether to wait for U-Boot to
220                 echo the command text back to its output.
221             send_nl: Boolean indicating whether to send a newline character
222                 after the command string.
223             wait_for_prompt: Boolean indicating whether to wait for the
224                 command prompt to be sent by U-Boot. This typically occurs
225                 immediately after the command has been executed.
226             wait_for_reboot: Boolean indication whether to wait for the
227                 reboot U-Boot. If this sets True, wait_for_prompt must also
228                 be True.
229
230         Returns:
231             If wait_for_prompt == False:
232                 Nothing.
233             Else:
234                 The output from U-Boot during command execution. In other
235                 words, the text U-Boot emitted between the point it echod the
236                 command string and emitted the subsequent command prompts.
237         """
238
239         if self.at_prompt and \
240                 self.at_prompt_logevt != self.logstream.logfile.cur_evt:
241             self.logstream.write(self.prompt, implicit=True)
242
243         try:
244             self.at_prompt = False
245             if send_nl:
246                 cmd += '\n'
247             while cmd:
248                 # Limit max outstanding data, so UART FIFOs don't overflow
249                 chunk = cmd[:self.max_fifo_fill]
250                 cmd = cmd[self.max_fifo_fill:]
251                 self.p.send(chunk)
252                 if not wait_for_echo:
253                     continue
254                 chunk = re.escape(chunk)
255                 chunk = chunk.replace('\\\n', '[\r\n]')
256                 m = self.p.expect([chunk] + self.bad_patterns)
257                 if m != 0:
258                     self.at_prompt = False
259                     raise Exception('Bad pattern found on console: ' +
260                                     self.bad_pattern_ids[m - 1])
261             if not wait_for_prompt:
262                 return
263             if wait_for_reboot:
264                 self.wait_for_boot_prompt()
265             else:
266                 m = self.p.expect([self.prompt_compiled] + self.bad_patterns)
267                 if m != 0:
268                     self.at_prompt = False
269                     raise Exception('Bad pattern found on console: ' +
270                                     self.bad_pattern_ids[m - 1])
271             self.at_prompt = True
272             self.at_prompt_logevt = self.logstream.logfile.cur_evt
273             # Only strip \r\n; space/TAB might be significant if testing
274             # indentation.
275             return self.p.before.strip('\r\n')
276         except Exception as ex:
277             self.log.error(str(ex))
278             self.cleanup_spawn()
279             raise
280         finally:
281             self.log.timestamp()
282
283     def run_command_list(self, cmds):
284         """Run a list of commands.
285
286         This is a helper function to call run_command() with default arguments
287         for each command in a list.
288
289         Args:
290             cmd: List of commands (each a string).
291         Returns:
292             A list of output strings from each command, one element for each
293             command.
294         """
295         output = []
296         for cmd in cmds:
297             output.append(self.run_command(cmd))
298         return output
299
300     def ctrlc(self):
301         """Send a CTRL-C character to U-Boot.
302
303         This is useful in order to stop execution of long-running synchronous
304         commands such as "ums".
305
306         Args:
307             None.
308
309         Returns:
310             Nothing.
311         """
312
313         self.log.action('Sending Ctrl-C')
314         self.run_command(chr(3), wait_for_echo=False, send_nl=False)
315
316     def wait_for(self, text):
317         """Wait for a pattern to be emitted by U-Boot.
318
319         This is useful when a long-running command such as "dfu" is executing,
320         and it periodically emits some text that should show up at a specific
321         location in the log file.
322
323         Args:
324             text: The text to wait for; either a string (containing raw text,
325                 not a regular expression) or an re object.
326
327         Returns:
328             Nothing.
329         """
330
331         if type(text) == type(''):
332             text = re.escape(text)
333         m = self.p.expect([text] + self.bad_patterns)
334         if m != 0:
335             raise Exception('Bad pattern found on console: ' +
336                             self.bad_pattern_ids[m - 1])
337
338     def drain_console(self):
339         """Read from and log the U-Boot console for a short time.
340
341         U-Boot's console output is only logged when the test code actively
342         waits for U-Boot to emit specific data. There are cases where tests
343         can fail without doing this. For example, if a test asks U-Boot to
344         enable USB device mode, then polls until a host-side device node
345         exists. In such a case, it is useful to log U-Boot's console output
346         in case U-Boot printed clues as to why the host-side even did not
347         occur. This function will do that.
348
349         Args:
350             None.
351
352         Returns:
353             Nothing.
354         """
355
356         # If we are already not connected to U-Boot, there's nothing to drain.
357         # This should only happen when a previous call to run_command() or
358         # wait_for() failed (and hence the output has already been logged), or
359         # the system is shutting down.
360         if not self.p:
361             return
362
363         orig_timeout = self.p.timeout
364         try:
365             # Drain the log for a relatively short time.
366             self.p.timeout = 1000
367             # Wait for something U-Boot will likely never send. This will
368             # cause the console output to be read and logged.
369             self.p.expect(['This should never match U-Boot output'])
370         except:
371             # We expect a timeout, since U-Boot won't print what we waited
372             # for. Squash it when it happens.
373             #
374             # Squash any other exception too. This function is only used to
375             # drain (and log) the U-Boot console output after a failed test.
376             # The U-Boot process will be restarted, or target board reset, once
377             # this function returns. So, we don't care about detecting any
378             # additional errors, so they're squashed so that the rest of the
379             # post-test-failure cleanup code can continue operation, and
380             # correctly terminate any log sections, etc.
381             pass
382         finally:
383             self.p.timeout = orig_timeout
384
385     def ensure_spawned(self, expect_reset=False):
386         """Ensure a connection to a correctly running U-Boot instance.
387
388         This may require spawning a new Sandbox process or resetting target
389         hardware, as defined by the implementation sub-class.
390
391         This is an internal function and should not be called directly.
392
393         Args:
394             expect_reset: Boolean indication whether this boot is expected
395                 to be reset while the 1st boot process after main boot before
396                 prompt. False by default.
397
398         Returns:
399             Nothing.
400         """
401
402         if self.p:
403             # Reset the console timeout value as some tests may change
404             # its default value during the execution
405             if not self.config.gdbserver:
406                 self.p.timeout = 30000
407             return
408         try:
409             self.log.start_section('Starting U-Boot')
410             self.at_prompt = False
411             self.p = self.get_spawn()
412             # Real targets can take a long time to scroll large amounts of
413             # text if LCD is enabled. This value may need tweaking in the
414             # future, possibly per-test to be optimal. This works for 'help'
415             # on board 'seaboard'.
416             if not self.config.gdbserver:
417                 self.p.timeout = 30000
418             self.p.logfile_read = self.logstream
419             if expect_reset:
420                 loop_num = 2
421             else:
422                 loop_num = 1
423             self.wait_for_boot_prompt(loop_num = loop_num)
424             self.at_prompt = True
425             self.at_prompt_logevt = self.logstream.logfile.cur_evt
426         except Exception as ex:
427             self.log.error(str(ex))
428             self.cleanup_spawn()
429             raise
430         finally:
431             self.log.timestamp()
432             self.log.end_section('Starting U-Boot')
433
434     def cleanup_spawn(self):
435         """Shut down all interaction with the U-Boot instance.
436
437         This is used when an error is detected prior to re-establishing a
438         connection with a fresh U-Boot instance.
439
440         This is an internal function and should not be called directly.
441
442         Args:
443             None.
444
445         Returns:
446             Nothing.
447         """
448
449         try:
450             if self.p:
451                 self.p.close()
452         except:
453             pass
454         self.p = None
455
456     def restart_uboot(self, expect_reset=False):
457         """Shut down and restart U-Boot."""
458         self.cleanup_spawn()
459         self.ensure_spawned(expect_reset)
460
461     def get_spawn_output(self):
462         """Return the start-up output from U-Boot
463
464         Returns:
465             The output produced by ensure_spawed(), as a string.
466         """
467         if self.p:
468             return self.p.get_expect_output()
469         return None
470
471     def validate_version_string_in_text(self, text):
472         """Assert that a command's output includes the U-Boot signon message.
473
474         This is primarily useful for validating the "version" command without
475         duplicating the signon text regex in a test function.
476
477         Args:
478             text: The command output text to check.
479
480         Returns:
481             Nothing. An exception is raised if the validation fails.
482         """
483
484         assert(self.u_boot_version_string in text)
485
486     def disable_check(self, check_type):
487         """Temporarily disable an error check of U-Boot's output.
488
489         Create a new context manager (for use with the "with" statement) which
490         temporarily disables a particular console output error check.
491
492         Args:
493             check_type: The type of error-check to disable. Valid values may
494             be found in self.disable_check_count above.
495
496         Returns:
497             A context manager object.
498         """
499
500         return ConsoleDisableCheck(self, check_type)
501
502     def temporary_timeout(self, timeout):
503         """Temporarily set up different timeout for commands.
504
505         Create a new context manager (for use with the "with" statement) which
506         temporarily change timeout.
507
508         Args:
509             timeout: Time in milliseconds.
510
511         Returns:
512             A context manager object.
513         """
514
515         return ConsoleSetupTimeout(self, timeout)