test/py: drain console log at the end of any failed test
[platform/kernel/u-boot.git] / test / py / u_boot_console_base.py
1 # Copyright (c) 2015 Stephen Warren
2 # Copyright (c) 2015-2016, NVIDIA CORPORATION. All rights reserved.
3 #
4 # SPDX-License-Identifier: GPL-2.0
5
6 # Common logic to interact with U-Boot via the console. This class provides
7 # the interface that tests use to execute U-Boot shell commands and wait for
8 # their results. Sub-classes exist to perform board-type-specific setup
9 # operations, such as spawning a sub-process for Sandbox, or attaching to the
10 # serial console of real hardware.
11
12 import multiplexed_log
13 import os
14 import pytest
15 import re
16 import sys
17 import u_boot_spawn
18
19 # Regexes for text we expect U-Boot to send to the console.
20 pattern_u_boot_spl_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
26 class ConsoleDisableCheck(object):
27     '''Context manager (for Python's with statement) that temporarily disables
28     the specified console output error check. This is useful when deliberately
29     executing a command that is known to trigger one of the error checks, in
30     order to test that the error condition is actually raised. This class is
31     used internally by ConsoleBase::disable_check(); it is not intended for
32     direct usage.'''
33
34     def __init__(self, console, check_type):
35         self.console = console
36         self.check_type = check_type
37
38     def __enter__(self):
39         self.console.disable_check_count[self.check_type] += 1
40
41     def __exit__(self, extype, value, traceback):
42         self.console.disable_check_count[self.check_type] -= 1
43
44 class ConsoleBase(object):
45     '''The interface through which test functions interact with the U-Boot
46     console. This primarily involves executing shell commands, capturing their
47     results, and checking for common error conditions. Some common utilities
48     are also provided too.'''
49
50     def __init__(self, log, config, max_fifo_fill):
51         '''Initialize a U-Boot console connection.
52
53         Can only usefully be called by sub-classes.
54
55         Args:
56             log: A mulptiplex_log.Logfile object, to which the U-Boot output
57                 will be logged.
58             config: A configuration data structure, as built by conftest.py.
59             max_fifo_fill: The maximum number of characters to send to U-Boot
60                 command-line before waiting for U-Boot to echo the characters
61                 back. For UART-based HW without HW flow control, this value
62                 should be set less than the UART RX FIFO size to avoid
63                 overflow, assuming that U-Boot can't keep up with full-rate
64                 traffic at the baud rate.
65
66         Returns:
67             Nothing.
68         '''
69
70         self.log = log
71         self.config = config
72         self.max_fifo_fill = max_fifo_fill
73
74         self.logstream = self.log.get_stream('console', sys.stdout)
75
76         # Array slice removes leading/trailing quotes
77         self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
78         self.prompt_escaped = re.escape(self.prompt)
79         self.p = None
80         self.disable_check_count = {
81             'spl_signon': 0,
82             'main_signon': 0,
83             'unknown_command': 0,
84             'error_notification': 0,
85         }
86
87         self.at_prompt = False
88         self.at_prompt_logevt = None
89         self.ram_base = None
90
91     def close(self):
92         '''Terminate the connection to the U-Boot console.
93
94         This function is only useful once all interaction with U-Boot is
95         complete. Once this function is called, data cannot be sent to or
96         received from U-Boot.
97
98         Args:
99             None.
100
101         Returns:
102             Nothing.
103         '''
104
105         if self.p:
106             self.p.close()
107         self.logstream.close()
108
109     def run_command(self, cmd, wait_for_echo=True, send_nl=True,
110             wait_for_prompt=True):
111         '''Execute a command via the U-Boot console.
112
113         The command is always sent to U-Boot.
114
115         U-Boot echoes any command back to its output, and this function
116         typically waits for that to occur. The wait can be disabled by setting
117         wait_for_echo=False, which is useful e.g. when sending CTRL-C to
118         interrupt a long-running command such as "ums".
119
120         Command execution is typically triggered by sending a newline
121         character. This can be disabled by setting send_nl=False, which is
122         also useful when sending CTRL-C.
123
124         This function typically waits for the command to finish executing, and
125         returns the console output that it generated. This can be disabled by
126         setting wait_for_prompt=False, which is useful when invoking a long-
127         running command such as "ums".
128
129         Args:
130             cmd: The command to send.
131             wait_for_each: Boolean indicating whether to wait for U-Boot to
132                 echo the command text back to its output.
133             send_nl: Boolean indicating whether to send a newline character
134                 after the command string.
135             wait_for_prompt: Boolean indicating whether to wait for the
136                 command prompt to be sent by U-Boot. This typically occurs
137                 immediately after the command has been executed.
138
139         Returns:
140             If wait_for_prompt == False:
141                 Nothing.
142             Else:
143                 The output from U-Boot during command execution. In other
144                 words, the text U-Boot emitted between the point it echod the
145                 command string and emitted the subsequent command prompts.
146         '''
147
148         if self.at_prompt and \
149                 self.at_prompt_logevt != self.logstream.logfile.cur_evt:
150             self.logstream.write(self.prompt, implicit=True)
151
152         bad_patterns = []
153         bad_pattern_ids = []
154         if (self.disable_check_count['spl_signon'] == 0 and
155                 self.u_boot_spl_signon):
156             bad_patterns.append(self.u_boot_spl_signon_escaped)
157             bad_pattern_ids.append('SPL signon')
158         if self.disable_check_count['main_signon'] == 0:
159             bad_patterns.append(self.u_boot_main_signon_escaped)
160             bad_pattern_ids.append('U-Boot main signon')
161         if self.disable_check_count['unknown_command'] == 0:
162             bad_patterns.append(pattern_unknown_command)
163             bad_pattern_ids.append('Unknown command')
164         if self.disable_check_count['error_notification'] == 0:
165             bad_patterns.append(pattern_error_notification)
166             bad_pattern_ids.append('Error notification')
167         try:
168             self.at_prompt = False
169             if send_nl:
170                 cmd += '\n'
171             while cmd:
172                 # Limit max outstanding data, so UART FIFOs don't overflow
173                 chunk = cmd[:self.max_fifo_fill]
174                 cmd = cmd[self.max_fifo_fill:]
175                 self.p.send(chunk)
176                 if not wait_for_echo:
177                     continue
178                 chunk = re.escape(chunk)
179                 chunk = chunk.replace('\\\n', '[\r\n]')
180                 m = self.p.expect([chunk] + bad_patterns)
181                 if m != 0:
182                     self.at_prompt = False
183                     raise Exception('Bad pattern found on console: ' +
184                                     bad_pattern_ids[m - 1])
185             if not wait_for_prompt:
186                 return
187             m = self.p.expect([self.prompt_escaped] + bad_patterns)
188             if m != 0:
189                 self.at_prompt = False
190                 raise Exception('Bad pattern found on console: ' +
191                                 bad_pattern_ids[m - 1])
192             self.at_prompt = True
193             self.at_prompt_logevt = self.logstream.logfile.cur_evt
194             # Only strip \r\n; space/TAB might be significant if testing
195             # indentation.
196             return self.p.before.strip('\r\n')
197         except Exception as ex:
198             self.log.error(str(ex))
199             self.cleanup_spawn()
200             raise
201
202     def ctrlc(self):
203         '''Send a CTRL-C character to U-Boot.
204
205         This is useful in order to stop execution of long-running synchronous
206         commands such as "ums".
207
208         Args:
209             None.
210
211         Returns:
212             Nothing.
213         '''
214
215         self.run_command(chr(3), wait_for_echo=False, send_nl=False)
216
217     def drain_console(self):
218         '''Read from and log the U-Boot console for a short time.
219
220         U-Boot's console output is only logged when the test code actively
221         waits for U-Boot to emit specific data. There are cases where tests
222         can fail without doing this. For example, if a test asks U-Boot to
223         enable USB device mode, then polls until a host-side device node
224         exists. In such a case, it is useful to log U-Boot's console output
225         in case U-Boot printed clues as to why the host-side even did not
226         occur. This function will do that.
227
228         Args:
229             None.
230
231         Returns:
232             Nothing.
233         '''
234
235         # If we are already not connected to U-Boot, there's nothing to drain.
236         # This should only happen when a previous call to run_command() or
237         # wait_for() failed (and hence the output has already been logged), or
238         # the system is shutting down.
239         if not self.p:
240             return
241
242         orig_timeout = self.p.timeout
243         try:
244             # Drain the log for a relatively short time.
245             self.p.timeout = 1000
246             # Wait for something U-Boot will likely never send. This will
247             # cause the console output to be read and logged.
248             self.p.expect(['This should never match U-Boot output'])
249         except u_boot_spawn.Timeout:
250             pass
251         finally:
252             self.p.timeout = orig_timeout
253
254     def ensure_spawned(self):
255         '''Ensure a connection to a correctly running U-Boot instance.
256
257         This may require spawning a new Sandbox process or resetting target
258         hardware, as defined by the implementation sub-class.
259
260         This is an internal function and should not be called directly.
261
262         Args:
263             None.
264
265         Returns:
266             Nothing.
267         '''
268
269         if self.p:
270             return
271         try:
272             self.at_prompt = False
273             self.log.action('Starting U-Boot')
274             self.p = self.get_spawn()
275             # Real targets can take a long time to scroll large amounts of
276             # text if LCD is enabled. This value may need tweaking in the
277             # future, possibly per-test to be optimal. This works for 'help'
278             # on board 'seaboard'.
279             self.p.timeout = 30000
280             self.p.logfile_read = self.logstream
281             if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
282                 self.p.expect([pattern_u_boot_spl_signon])
283                 self.u_boot_spl_signon = self.p.after
284                 self.u_boot_spl_signon_escaped = re.escape(self.p.after)
285             else:
286                 self.u_boot_spl_signon = None
287             self.p.expect([pattern_u_boot_main_signon])
288             self.u_boot_main_signon = self.p.after
289             self.u_boot_main_signon_escaped = re.escape(self.p.after)
290             build_idx = self.u_boot_main_signon.find(', Build:')
291             if build_idx == -1:
292                 self.u_boot_version_string = self.u_boot_main_signon
293             else:
294                 self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
295             while True:
296                 match = self.p.expect([self.prompt_escaped,
297                                        pattern_stop_autoboot_prompt])
298                 if match == 1:
299                     self.p.send(chr(3)) # CTRL-C
300                     continue
301                 break
302             self.at_prompt = True
303             self.at_prompt_logevt = self.logstream.logfile.cur_evt
304         except Exception as ex:
305             self.log.error(str(ex))
306             self.cleanup_spawn()
307             raise
308
309     def cleanup_spawn(self):
310         '''Shut down all interaction with the U-Boot instance.
311
312         This is used when an error is detected prior to re-establishing a
313         connection with a fresh U-Boot instance.
314
315         This is an internal function and should not be called directly.
316
317         Args:
318             None.
319
320         Returns:
321             Nothing.
322         '''
323
324         try:
325             if self.p:
326                 self.p.close()
327         except:
328             pass
329         self.p = None
330
331     def validate_version_string_in_text(self, text):
332         '''Assert that a command's output includes the U-Boot signon message.
333
334         This is primarily useful for validating the "version" command without
335         duplicating the signon text regex in a test function.
336
337         Args:
338             text: The command output text to check.
339
340         Returns:
341             Nothing. An exception is raised if the validation fails.
342         '''
343
344         assert(self.u_boot_version_string in text)
345
346     def disable_check(self, check_type):
347         '''Temporarily disable an error check of U-Boot's output.
348
349         Create a new context manager (for use with the "with" statement) which
350         temporarily disables a particular console output error check.
351
352         Args:
353             check_type: The type of error-check to disable. Valid values may
354             be found in self.disable_check_count above.
355
356         Returns:
357             A context manager object.
358         '''
359
360         return ConsoleDisableCheck(self, check_type)
361
362     def find_ram_base(self):
363         '''Find the running U-Boot's RAM location.
364
365         Probe the running U-Boot to determine the address of the first bank
366         of RAM. This is useful for tests that test reading/writing RAM, or
367         load/save files that aren't associated with some standard address
368         typically represented in an environment variable such as
369         ${kernel_addr_r}. The value is cached so that it only needs to be
370         actively read once.
371
372         Args:
373             None.
374
375         Returns:
376             The address of U-Boot's first RAM bank, as an integer.
377         '''
378
379         if self.config.buildconfig.get('config_cmd_bdi', 'n') != 'y':
380             pytest.skip('bdinfo command not supported')
381         if self.ram_base == -1:
382             pytest.skip('Previously failed to find RAM bank start')
383         if self.ram_base is not None:
384             return self.ram_base
385
386         with self.log.section('find_ram_base'):
387             response = self.run_command('bdinfo')
388             for l in response.split('\n'):
389                 if '-> start' in l:
390                     self.ram_base = int(l.split('=')[1].strip(), 16)
391                     break
392             if self.ram_base is None:
393                 self.ram_base = -1
394                 raise Exception('Failed to find RAM bank start in `bdinfo`')
395
396         return self.ram_base