520f9a9e9f311d1b66d022ae14016344ee4b0e0f
[oweals/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
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_main_signon = re.compile('(U-Boot \\d{4}\\.\\d{2}-[^\r\n]*)')
21 pattern_stop_autoboot_prompt = re.compile('Hit any key to stop autoboot: ')
22 pattern_unknown_command = re.compile('Unknown command \'.*\' - try \'help\'')
23 pattern_error_notification = re.compile('## Error: ')
24
25 class ConsoleDisableCheck(object):
26     '''Context manager (for Python's with statement) that temporarily disables
27     the specified console output error check. This is useful when deliberately
28     executing a command that is known to trigger one of the error checks, in
29     order to test that the error condition is actually raised. This class is
30     used internally by ConsoleBase::disable_check(); it is not intended for
31     direct usage.'''
32
33     def __init__(self, console, check_type):
34         self.console = console
35         self.check_type = check_type
36
37     def __enter__(self):
38         self.console.disable_check_count[self.check_type] += 1
39
40     def __exit__(self, extype, value, traceback):
41         self.console.disable_check_count[self.check_type] -= 1
42
43 class ConsoleBase(object):
44     '''The interface through which test functions interact with the U-Boot
45     console. This primarily involves executing shell commands, capturing their
46     results, and checking for common error conditions. Some common utilities
47     are also provided too.'''
48
49     def __init__(self, log, config, max_fifo_fill):
50         '''Initialize a U-Boot console connection.
51
52         Can only usefully be called by sub-classes.
53
54         Args:
55             log: A mulptiplex_log.Logfile object, to which the U-Boot output
56                 will be logged.
57             config: A configuration data structure, as built by conftest.py.
58             max_fifo_fill: The maximum number of characters to send to U-Boot
59                 command-line before waiting for U-Boot to echo the characters
60                 back. For UART-based HW without HW flow control, this value
61                 should be set less than the UART RX FIFO size to avoid
62                 overflow, assuming that U-Boot can't keep up with full-rate
63                 traffic at the baud rate.
64
65         Returns:
66             Nothing.
67         '''
68
69         self.log = log
70         self.config = config
71         self.max_fifo_fill = max_fifo_fill
72
73         self.logstream = self.log.get_stream('console', sys.stdout)
74
75         # Array slice removes leading/trailing quotes
76         self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
77         self.prompt_escaped = re.escape(self.prompt)
78         self.p = None
79         self.disable_check_count = {
80             'spl_signon': 0,
81             'main_signon': 0,
82             'unknown_command': 0,
83             'error_notification': 0,
84         }
85
86         self.at_prompt = False
87         self.at_prompt_logevt = None
88         self.ram_base = 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         self.ensure_spawned()
148
149         if self.at_prompt and \
150                 self.at_prompt_logevt != self.logstream.logfile.cur_evt:
151             self.logstream.write(self.prompt, implicit=True)
152
153         bad_patterns = []
154         bad_pattern_ids = []
155         if (self.disable_check_count['spl_signon'] == 0 and
156                 self.u_boot_spl_signon):
157             bad_patterns.append(self.u_boot_spl_signon_escaped)
158             bad_pattern_ids.append('SPL signon')
159         if self.disable_check_count['main_signon'] == 0:
160             bad_patterns.append(self.u_boot_main_signon_escaped)
161             bad_pattern_ids.append('U-Boot main signon')
162         if self.disable_check_count['unknown_command'] == 0:
163             bad_patterns.append(pattern_unknown_command)
164             bad_pattern_ids.append('Unknown command')
165         if self.disable_check_count['error_notification'] == 0:
166             bad_patterns.append(pattern_error_notification)
167             bad_pattern_ids.append('Error notification')
168         try:
169             self.at_prompt = False
170             if send_nl:
171                 cmd += '\n'
172             while cmd:
173                 # Limit max outstanding data, so UART FIFOs don't overflow
174                 chunk = cmd[:self.max_fifo_fill]
175                 cmd = cmd[self.max_fifo_fill:]
176                 self.p.send(chunk)
177                 if not wait_for_echo:
178                     continue
179                 chunk = re.escape(chunk)
180                 chunk = chunk.replace('\\\n', '[\r\n]')
181                 m = self.p.expect([chunk] + bad_patterns)
182                 if m != 0:
183                     self.at_prompt = False
184                     raise Exception('Bad pattern found on console: ' +
185                                     bad_pattern_ids[m - 1])
186             if not wait_for_prompt:
187                 return
188             m = self.p.expect([self.prompt_escaped] + bad_patterns)
189             if m != 0:
190                 self.at_prompt = False
191                 raise Exception('Bad pattern found on console: ' +
192                                 bad_pattern_ids[m - 1])
193             self.at_prompt = True
194             self.at_prompt_logevt = self.logstream.logfile.cur_evt
195             # Only strip \r\n; space/TAB might be significant if testing
196             # indentation.
197             return self.p.before.strip('\r\n')
198         except Exception as ex:
199             self.log.error(str(ex))
200             self.cleanup_spawn()
201             raise
202
203     def ctrlc(self):
204         '''Send a CTRL-C character to U-Boot.
205
206         This is useful in order to stop execution of long-running synchronous
207         commands such as "ums".
208
209         Args:
210             None.
211
212         Returns:
213             Nothing.
214         '''
215
216         self.run_command(chr(3), wait_for_echo=False, send_nl=False)
217
218     def ensure_spawned(self):
219         '''Ensure a connection to a correctly running U-Boot instance.
220
221         This may require spawning a new Sandbox process or resetting target
222         hardware, as defined by the implementation sub-class.
223
224         This is an internal function and should not be called directly.
225
226         Args:
227             None.
228
229         Returns:
230             Nothing.
231         '''
232
233         if self.p:
234             return
235         try:
236             self.at_prompt = False
237             self.log.action('Starting U-Boot')
238             self.p = self.get_spawn()
239             # Real targets can take a long time to scroll large amounts of
240             # text if LCD is enabled. This value may need tweaking in the
241             # future, possibly per-test to be optimal. This works for 'help'
242             # on board 'seaboard'.
243             self.p.timeout = 30000
244             self.p.logfile_read = self.logstream
245             if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
246                 self.p.expect([pattern_u_boot_spl_signon])
247                 self.u_boot_spl_signon = self.p.after
248                 self.u_boot_spl_signon_escaped = re.escape(self.p.after)
249             else:
250                 self.u_boot_spl_signon = None
251             self.p.expect([pattern_u_boot_main_signon])
252             self.u_boot_main_signon = self.p.after
253             self.u_boot_main_signon_escaped = re.escape(self.p.after)
254             build_idx = self.u_boot_main_signon.find(', Build:')
255             if build_idx == -1:
256                 self.u_boot_version_string = self.u_boot_main_signon
257             else:
258                 self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
259             while True:
260                 match = self.p.expect([self.prompt_escaped,
261                                        pattern_stop_autoboot_prompt])
262                 if match == 1:
263                     self.p.send(chr(3)) # CTRL-C
264                     continue
265                 break
266             self.at_prompt = True
267             self.at_prompt_logevt = self.logstream.logfile.cur_evt
268         except Exception as ex:
269             self.log.error(str(ex))
270             self.cleanup_spawn()
271             raise
272
273     def cleanup_spawn(self):
274         '''Shut down all interaction with the U-Boot instance.
275
276         This is used when an error is detected prior to re-establishing a
277         connection with a fresh U-Boot instance.
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         try:
289             if self.p:
290                 self.p.close()
291         except:
292             pass
293         self.p = None
294
295     def validate_version_string_in_text(self, text):
296         '''Assert that a command's output includes the U-Boot signon message.
297
298         This is primarily useful for validating the "version" command without
299         duplicating the signon text regex in a test function.
300
301         Args:
302             text: The command output text to check.
303
304         Returns:
305             Nothing. An exception is raised if the validation fails.
306         '''
307
308         assert(self.u_boot_version_string in text)
309
310     def disable_check(self, check_type):
311         '''Temporarily disable an error check of U-Boot's output.
312
313         Create a new context manager (for use with the "with" statement) which
314         temporarily disables a particular console output error check.
315
316         Args:
317             check_type: The type of error-check to disable. Valid values may
318             be found in self.disable_check_count above.
319
320         Returns:
321             A context manager object.
322         '''
323
324         return ConsoleDisableCheck(self, check_type)
325
326     def find_ram_base(self):
327         '''Find the running U-Boot's RAM location.
328
329         Probe the running U-Boot to determine the address of the first bank
330         of RAM. This is useful for tests that test reading/writing RAM, or
331         load/save files that aren't associated with some standard address
332         typically represented in an environment variable such as
333         ${kernel_addr_r}. The value is cached so that it only needs to be
334         actively read once.
335
336         Args:
337             None.
338
339         Returns:
340             The address of U-Boot's first RAM bank, as an integer.
341         '''
342
343         if self.config.buildconfig.get('config_cmd_bdi', 'n') != 'y':
344             pytest.skip('bdinfo command not supported')
345         if self.ram_base == -1:
346             pytest.skip('Previously failed to find RAM bank start')
347         if self.ram_base is not None:
348             return self.ram_base
349
350         with self.log.section('find_ram_base'):
351             response = self.run_command('bdinfo')
352             for l in response.split('\n'):
353                 if '-> start' in l:
354                     self.ram_base = int(l.split('=')[1].strip(), 16)
355                     break
356             if self.ram_base is None:
357                 self.ram_base = -1
358                 raise Exception('Failed to find RAM bank start in `bdinfo`')
359
360         return self.ram_base