test/py: log when tests send CTRL-C
[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.log.action('Sending Ctrl-C')
216         self.run_command(chr(3), wait_for_echo=False, send_nl=False)
217
218     def drain_console(self):
219         '''Read from and log the U-Boot console for a short time.
220
221         U-Boot's console output is only logged when the test code actively
222         waits for U-Boot to emit specific data. There are cases where tests
223         can fail without doing this. For example, if a test asks U-Boot to
224         enable USB device mode, then polls until a host-side device node
225         exists. In such a case, it is useful to log U-Boot's console output
226         in case U-Boot printed clues as to why the host-side even did not
227         occur. This function will do that.
228
229         Args:
230             None.
231
232         Returns:
233             Nothing.
234         '''
235
236         # If we are already not connected to U-Boot, there's nothing to drain.
237         # This should only happen when a previous call to run_command() or
238         # wait_for() failed (and hence the output has already been logged), or
239         # the system is shutting down.
240         if not self.p:
241             return
242
243         orig_timeout = self.p.timeout
244         try:
245             # Drain the log for a relatively short time.
246             self.p.timeout = 1000
247             # Wait for something U-Boot will likely never send. This will
248             # cause the console output to be read and logged.
249             self.p.expect(['This should never match U-Boot output'])
250         except u_boot_spawn.Timeout:
251             pass
252         finally:
253             self.p.timeout = orig_timeout
254
255     def ensure_spawned(self):
256         '''Ensure a connection to a correctly running U-Boot instance.
257
258         This may require spawning a new Sandbox process or resetting target
259         hardware, as defined by the implementation sub-class.
260
261         This is an internal function and should not be called directly.
262
263         Args:
264             None.
265
266         Returns:
267             Nothing.
268         '''
269
270         if self.p:
271             return
272         try:
273             self.at_prompt = False
274             self.log.action('Starting U-Boot')
275             self.p = self.get_spawn()
276             # Real targets can take a long time to scroll large amounts of
277             # text if LCD is enabled. This value may need tweaking in the
278             # future, possibly per-test to be optimal. This works for 'help'
279             # on board 'seaboard'.
280             self.p.timeout = 30000
281             self.p.logfile_read = self.logstream
282             if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
283                 self.p.expect([pattern_u_boot_spl_signon])
284                 self.u_boot_spl_signon = self.p.after
285                 self.u_boot_spl_signon_escaped = re.escape(self.p.after)
286             else:
287                 self.u_boot_spl_signon = None
288             self.p.expect([pattern_u_boot_main_signon])
289             self.u_boot_main_signon = self.p.after
290             self.u_boot_main_signon_escaped = re.escape(self.p.after)
291             build_idx = self.u_boot_main_signon.find(', Build:')
292             if build_idx == -1:
293                 self.u_boot_version_string = self.u_boot_main_signon
294             else:
295                 self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
296             while True:
297                 match = self.p.expect([self.prompt_escaped,
298                                        pattern_stop_autoboot_prompt])
299                 if match == 1:
300                     self.p.send(chr(3)) # CTRL-C
301                     continue
302                 break
303             self.at_prompt = True
304             self.at_prompt_logevt = self.logstream.logfile.cur_evt
305         except Exception as ex:
306             self.log.error(str(ex))
307             self.cleanup_spawn()
308             raise
309
310     def cleanup_spawn(self):
311         '''Shut down all interaction with the U-Boot instance.
312
313         This is used when an error is detected prior to re-establishing a
314         connection with a fresh U-Boot instance.
315
316         This is an internal function and should not be called directly.
317
318         Args:
319             None.
320
321         Returns:
322             Nothing.
323         '''
324
325         try:
326             if self.p:
327                 self.p.close()
328         except:
329             pass
330         self.p = None
331
332     def validate_version_string_in_text(self, text):
333         '''Assert that a command's output includes the U-Boot signon message.
334
335         This is primarily useful for validating the "version" command without
336         duplicating the signon text regex in a test function.
337
338         Args:
339             text: The command output text to check.
340
341         Returns:
342             Nothing. An exception is raised if the validation fails.
343         '''
344
345         assert(self.u_boot_version_string in text)
346
347     def disable_check(self, check_type):
348         '''Temporarily disable an error check of U-Boot's output.
349
350         Create a new context manager (for use with the "with" statement) which
351         temporarily disables a particular console output error check.
352
353         Args:
354             check_type: The type of error-check to disable. Valid values may
355             be found in self.disable_check_count above.
356
357         Returns:
358             A context manager object.
359         '''
360
361         return ConsoleDisableCheck(self, check_type)
362
363     def find_ram_base(self):
364         '''Find the running U-Boot's RAM location.
365
366         Probe the running U-Boot to determine the address of the first bank
367         of RAM. This is useful for tests that test reading/writing RAM, or
368         load/save files that aren't associated with some standard address
369         typically represented in an environment variable such as
370         ${kernel_addr_r}. The value is cached so that it only needs to be
371         actively read once.
372
373         Args:
374             None.
375
376         Returns:
377             The address of U-Boot's first RAM bank, as an integer.
378         '''
379
380         if self.config.buildconfig.get('config_cmd_bdi', 'n') != 'y':
381             pytest.skip('bdinfo command not supported')
382         if self.ram_base == -1:
383             pytest.skip('Previously failed to find RAM bank start')
384         if self.ram_base is not None:
385             return self.ram_base
386
387         with self.log.section('find_ram_base'):
388             response = self.run_command('bdinfo')
389             for l in response.split('\n'):
390                 if '-> start' in l:
391                     self.ram_base = int(l.split('=')[1].strip(), 16)
392                     break
393             if self.ram_base is None:
394                 self.ram_base = -1
395                 raise Exception('Failed to find RAM bank start in `bdinfo`')
396
397         return self.ram_base