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