51163bc0db68d5e291a5d1fd4622a5f817cba02b
[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
90     def close(self):
91         '''Terminate the connection to the U-Boot console.
92
93         This function is only useful once all interaction with U-Boot is
94         complete. Once this function is called, data cannot be sent to or
95         received from U-Boot.
96
97         Args:
98             None.
99
100         Returns:
101             Nothing.
102         '''
103
104         if self.p:
105             self.p.close()
106         self.logstream.close()
107
108     def run_command(self, cmd, wait_for_echo=True, send_nl=True,
109             wait_for_prompt=True):
110         '''Execute a command via the U-Boot console.
111
112         The command is always sent to U-Boot.
113
114         U-Boot echoes any command back to its output, and this function
115         typically waits for that to occur. The wait can be disabled by setting
116         wait_for_echo=False, which is useful e.g. when sending CTRL-C to
117         interrupt a long-running command such as "ums".
118
119         Command execution is typically triggered by sending a newline
120         character. This can be disabled by setting send_nl=False, which is
121         also useful when sending CTRL-C.
122
123         This function typically waits for the command to finish executing, and
124         returns the console output that it generated. This can be disabled by
125         setting wait_for_prompt=False, which is useful when invoking a long-
126         running command such as "ums".
127
128         Args:
129             cmd: The command to send.
130             wait_for_each: Boolean indicating whether to wait for U-Boot to
131                 echo the command text back to its output.
132             send_nl: Boolean indicating whether to send a newline character
133                 after the command string.
134             wait_for_prompt: Boolean indicating whether to wait for the
135                 command prompt to be sent by U-Boot. This typically occurs
136                 immediately after the command has been executed.
137
138         Returns:
139             If wait_for_prompt == False:
140                 Nothing.
141             Else:
142                 The output from U-Boot during command execution. In other
143                 words, the text U-Boot emitted between the point it echod the
144                 command string and emitted the subsequent command prompts.
145         '''
146
147         if self.at_prompt and \
148                 self.at_prompt_logevt != self.logstream.logfile.cur_evt:
149             self.logstream.write(self.prompt, implicit=True)
150
151         bad_patterns = []
152         bad_pattern_ids = []
153         if (self.disable_check_count['spl_signon'] == 0 and
154                 self.u_boot_spl_signon):
155             bad_patterns.append(self.u_boot_spl_signon_escaped)
156             bad_pattern_ids.append('SPL signon')
157         if self.disable_check_count['main_signon'] == 0:
158             bad_patterns.append(self.u_boot_main_signon_escaped)
159             bad_pattern_ids.append('U-Boot main signon')
160         if self.disable_check_count['unknown_command'] == 0:
161             bad_patterns.append(pattern_unknown_command)
162             bad_pattern_ids.append('Unknown command')
163         if self.disable_check_count['error_notification'] == 0:
164             bad_patterns.append(pattern_error_notification)
165             bad_pattern_ids.append('Error notification')
166         try:
167             self.at_prompt = False
168             if send_nl:
169                 cmd += '\n'
170             while cmd:
171                 # Limit max outstanding data, so UART FIFOs don't overflow
172                 chunk = cmd[:self.max_fifo_fill]
173                 cmd = cmd[self.max_fifo_fill:]
174                 self.p.send(chunk)
175                 if not wait_for_echo:
176                     continue
177                 chunk = re.escape(chunk)
178                 chunk = chunk.replace('\\\n', '[\r\n]')
179                 m = self.p.expect([chunk] + bad_patterns)
180                 if m != 0:
181                     self.at_prompt = False
182                     raise Exception('Bad pattern found on console: ' +
183                                     bad_pattern_ids[m - 1])
184             if not wait_for_prompt:
185                 return
186             m = self.p.expect([self.prompt_escaped] + bad_patterns)
187             if m != 0:
188                 self.at_prompt = False
189                 raise Exception('Bad pattern found on console: ' +
190                                 bad_pattern_ids[m - 1])
191             self.at_prompt = True
192             self.at_prompt_logevt = self.logstream.logfile.cur_evt
193             # Only strip \r\n; space/TAB might be significant if testing
194             # indentation.
195             return self.p.before.strip('\r\n')
196         except Exception as ex:
197             self.log.error(str(ex))
198             self.cleanup_spawn()
199             raise
200
201     def ctrlc(self):
202         '''Send a CTRL-C character to U-Boot.
203
204         This is useful in order to stop execution of long-running synchronous
205         commands such as "ums".
206
207         Args:
208             None.
209
210         Returns:
211             Nothing.
212         '''
213
214         self.log.action('Sending Ctrl-C')
215         self.run_command(chr(3), wait_for_echo=False, send_nl=False)
216
217     def wait_for(self, text):
218         '''Wait for a pattern to be emitted by U-Boot.
219
220         This is useful when a long-running command such as "dfu" is executing,
221         and it periodically emits some text that should show up at a specific
222         location in the log file.
223
224         Args:
225             text: The text to wait for; either a string (containing raw text,
226                 not a regular expression) or an re object.
227
228         Returns:
229             Nothing.
230         '''
231
232         if type(text) == type(''):
233             text = re.escape(text)
234         self.p.expect([text])
235
236     def drain_console(self):
237         '''Read from and log the U-Boot console for a short time.
238
239         U-Boot's console output is only logged when the test code actively
240         waits for U-Boot to emit specific data. There are cases where tests
241         can fail without doing this. For example, if a test asks U-Boot to
242         enable USB device mode, then polls until a host-side device node
243         exists. In such a case, it is useful to log U-Boot's console output
244         in case U-Boot printed clues as to why the host-side even did not
245         occur. This function will do that.
246
247         Args:
248             None.
249
250         Returns:
251             Nothing.
252         '''
253
254         # If we are already not connected to U-Boot, there's nothing to drain.
255         # This should only happen when a previous call to run_command() or
256         # wait_for() failed (and hence the output has already been logged), or
257         # the system is shutting down.
258         if not self.p:
259             return
260
261         orig_timeout = self.p.timeout
262         try:
263             # Drain the log for a relatively short time.
264             self.p.timeout = 1000
265             # Wait for something U-Boot will likely never send. This will
266             # cause the console output to be read and logged.
267             self.p.expect(['This should never match U-Boot output'])
268         except u_boot_spawn.Timeout:
269             pass
270         finally:
271             self.p.timeout = orig_timeout
272
273     def ensure_spawned(self):
274         '''Ensure a connection to a correctly running U-Boot instance.
275
276         This may require spawning a new Sandbox process or resetting target
277         hardware, as defined by the implementation sub-class.
278
279         This is an internal function and should not be called directly.
280
281         Args:
282             None.
283
284         Returns:
285             Nothing.
286         '''
287
288         if self.p:
289             return
290         try:
291             self.at_prompt = False
292             self.log.action('Starting U-Boot')
293             self.p = self.get_spawn()
294             # Real targets can take a long time to scroll large amounts of
295             # text if LCD is enabled. This value may need tweaking in the
296             # future, possibly per-test to be optimal. This works for 'help'
297             # on board 'seaboard'.
298             self.p.timeout = 30000
299             self.p.logfile_read = self.logstream
300             if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
301                 self.p.expect([pattern_u_boot_spl_signon])
302                 self.u_boot_spl_signon = self.p.after
303                 self.u_boot_spl_signon_escaped = re.escape(self.p.after)
304             else:
305                 self.u_boot_spl_signon = None
306             self.p.expect([pattern_u_boot_main_signon])
307             self.u_boot_main_signon = self.p.after
308             self.u_boot_main_signon_escaped = re.escape(self.p.after)
309             build_idx = self.u_boot_main_signon.find(', Build:')
310             if build_idx == -1:
311                 self.u_boot_version_string = self.u_boot_main_signon
312             else:
313                 self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
314             while True:
315                 match = self.p.expect([self.prompt_escaped,
316                                        pattern_stop_autoboot_prompt])
317                 if match == 1:
318                     self.p.send(chr(3)) # CTRL-C
319                     continue
320                 break
321             self.at_prompt = True
322             self.at_prompt_logevt = self.logstream.logfile.cur_evt
323         except Exception as ex:
324             self.log.error(str(ex))
325             self.cleanup_spawn()
326             raise
327
328     def cleanup_spawn(self):
329         '''Shut down all interaction with the U-Boot instance.
330
331         This is used when an error is detected prior to re-establishing a
332         connection with a fresh U-Boot instance.
333
334         This is an internal function and should not be called directly.
335
336         Args:
337             None.
338
339         Returns:
340             Nothing.
341         '''
342
343         try:
344             if self.p:
345                 self.p.close()
346         except:
347             pass
348         self.p = None
349
350     def validate_version_string_in_text(self, text):
351         '''Assert that a command's output includes the U-Boot signon message.
352
353         This is primarily useful for validating the "version" command without
354         duplicating the signon text regex in a test function.
355
356         Args:
357             text: The command output text to check.
358
359         Returns:
360             Nothing. An exception is raised if the validation fails.
361         '''
362
363         assert(self.u_boot_version_string in text)
364
365     def disable_check(self, check_type):
366         '''Temporarily disable an error check of U-Boot's output.
367
368         Create a new context manager (for use with the "with" statement) which
369         temporarily disables a particular console output error check.
370
371         Args:
372             check_type: The type of error-check to disable. Valid values may
373             be found in self.disable_check_count above.
374
375         Returns:
376             A context manager object.
377         '''
378
379         return ConsoleDisableCheck(self, check_type)