test/py: use space to interrupt autoboot
[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 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 pattern_error_please_reset = re.compile('### ERROR ### Please RESET the board ###')
26
27 PAT_ID = 0
28 PAT_RE = 1
29
30 bad_pattern_defs = (
31     ('spl_signon', pattern_u_boot_spl_signon),
32     ('main_signon', pattern_u_boot_main_signon),
33     ('stop_autoboot_prompt', pattern_stop_autoboot_prompt),
34     ('unknown_command', pattern_unknown_command),
35     ('error_notification', pattern_error_notification),
36     ('error_please_reset', pattern_error_please_reset),
37 )
38
39 class ConsoleDisableCheck(object):
40     """Context manager (for Python's with statement) that temporarily disables
41     the specified console output error check. This is useful when deliberately
42     executing a command that is known to trigger one of the error checks, in
43     order to test that the error condition is actually raised. This class is
44     used internally by ConsoleBase::disable_check(); it is not intended for
45     direct usage."""
46
47     def __init__(self, console, check_type):
48         self.console = console
49         self.check_type = check_type
50
51     def __enter__(self):
52         self.console.disable_check_count[self.check_type] += 1
53         self.console.eval_bad_patterns()
54
55     def __exit__(self, extype, value, traceback):
56         self.console.disable_check_count[self.check_type] -= 1
57         self.console.eval_bad_patterns()
58
59 class ConsoleBase(object):
60     """The interface through which test functions interact with the U-Boot
61     console. This primarily involves executing shell commands, capturing their
62     results, and checking for common error conditions. Some common utilities
63     are also provided too."""
64
65     def __init__(self, log, config, max_fifo_fill):
66         """Initialize a U-Boot console connection.
67
68         Can only usefully be called by sub-classes.
69
70         Args:
71             log: A mulptiplex_log.Logfile object, to which the U-Boot output
72                 will be logged.
73             config: A configuration data structure, as built by conftest.py.
74             max_fifo_fill: The maximum number of characters to send to U-Boot
75                 command-line before waiting for U-Boot to echo the characters
76                 back. For UART-based HW without HW flow control, this value
77                 should be set less than the UART RX FIFO size to avoid
78                 overflow, assuming that U-Boot can't keep up with full-rate
79                 traffic at the baud rate.
80
81         Returns:
82             Nothing.
83         """
84
85         self.log = log
86         self.config = config
87         self.max_fifo_fill = max_fifo_fill
88
89         self.logstream = self.log.get_stream('console', sys.stdout)
90
91         # Array slice removes leading/trailing quotes
92         self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
93         self.prompt_escaped = re.escape(self.prompt)
94         self.p = None
95         self.disable_check_count = {pat[PAT_ID]: 0 for pat in bad_pattern_defs}
96         self.eval_bad_patterns()
97
98         self.at_prompt = False
99         self.at_prompt_logevt = None
100
101     def eval_bad_patterns(self):
102         self.bad_patterns = [pat[PAT_RE] for pat in bad_pattern_defs \
103             if self.disable_check_count[pat[PAT_ID]] == 0]
104         self.bad_pattern_ids = [pat[PAT_ID] for pat in bad_pattern_defs \
105             if self.disable_check_count[pat[PAT_ID]] == 0]
106
107     def close(self):
108         """Terminate the connection to the U-Boot console.
109
110         This function is only useful once all interaction with U-Boot is
111         complete. Once this function is called, data cannot be sent to or
112         received from U-Boot.
113
114         Args:
115             None.
116
117         Returns:
118             Nothing.
119         """
120
121         if self.p:
122             self.p.close()
123         self.logstream.close()
124
125     def run_command(self, cmd, wait_for_echo=True, send_nl=True,
126             wait_for_prompt=True):
127         """Execute a command via the U-Boot console.
128
129         The command is always sent to U-Boot.
130
131         U-Boot echoes any command back to its output, and this function
132         typically waits for that to occur. The wait can be disabled by setting
133         wait_for_echo=False, which is useful e.g. when sending CTRL-C to
134         interrupt a long-running command such as "ums".
135
136         Command execution is typically triggered by sending a newline
137         character. This can be disabled by setting send_nl=False, which is
138         also useful when sending CTRL-C.
139
140         This function typically waits for the command to finish executing, and
141         returns the console output that it generated. This can be disabled by
142         setting wait_for_prompt=False, which is useful when invoking a long-
143         running command such as "ums".
144
145         Args:
146             cmd: The command to send.
147             wait_for_each: Boolean indicating whether to wait for U-Boot to
148                 echo the command text back to its output.
149             send_nl: Boolean indicating whether to send a newline character
150                 after the command string.
151             wait_for_prompt: Boolean indicating whether to wait for the
152                 command prompt to be sent by U-Boot. This typically occurs
153                 immediately after the command has been executed.
154
155         Returns:
156             If wait_for_prompt == False:
157                 Nothing.
158             Else:
159                 The output from U-Boot during command execution. In other
160                 words, the text U-Boot emitted between the point it echod the
161                 command string and emitted the subsequent command prompts.
162         """
163
164         if self.at_prompt and \
165                 self.at_prompt_logevt != self.logstream.logfile.cur_evt:
166             self.logstream.write(self.prompt, implicit=True)
167
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] + self.bad_patterns)
182                 if m != 0:
183                     self.at_prompt = False
184                     raise Exception('Bad pattern found on console: ' +
185                                     self.bad_pattern_ids[m - 1])
186             if not wait_for_prompt:
187                 return
188             m = self.p.expect([self.prompt_escaped] + self.bad_patterns)
189             if m != 0:
190                 self.at_prompt = False
191                 raise Exception('Bad pattern found on console: ' +
192                                 self.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.log.action('Sending Ctrl-C')
217         self.run_command(chr(3), wait_for_echo=False, send_nl=False)
218
219     def wait_for(self, text):
220         """Wait for a pattern to be emitted by U-Boot.
221
222         This is useful when a long-running command such as "dfu" is executing,
223         and it periodically emits some text that should show up at a specific
224         location in the log file.
225
226         Args:
227             text: The text to wait for; either a string (containing raw text,
228                 not a regular expression) or an re object.
229
230         Returns:
231             Nothing.
232         """
233
234         if type(text) == type(''):
235             text = re.escape(text)
236         m = self.p.expect([text] + self.bad_patterns)
237         if m != 0:
238             raise Exception('Bad pattern found on console: ' +
239                             self.bad_pattern_ids[m - 1])
240
241     def drain_console(self):
242         """Read from and log the U-Boot console for a short time.
243
244         U-Boot's console output is only logged when the test code actively
245         waits for U-Boot to emit specific data. There are cases where tests
246         can fail without doing this. For example, if a test asks U-Boot to
247         enable USB device mode, then polls until a host-side device node
248         exists. In such a case, it is useful to log U-Boot's console output
249         in case U-Boot printed clues as to why the host-side even did not
250         occur. This function will do that.
251
252         Args:
253             None.
254
255         Returns:
256             Nothing.
257         """
258
259         # If we are already not connected to U-Boot, there's nothing to drain.
260         # This should only happen when a previous call to run_command() or
261         # wait_for() failed (and hence the output has already been logged), or
262         # the system is shutting down.
263         if not self.p:
264             return
265
266         orig_timeout = self.p.timeout
267         try:
268             # Drain the log for a relatively short time.
269             self.p.timeout = 1000
270             # Wait for something U-Boot will likely never send. This will
271             # cause the console output to be read and logged.
272             self.p.expect(['This should never match U-Boot output'])
273         except u_boot_spawn.Timeout:
274             pass
275         finally:
276             self.p.timeout = orig_timeout
277
278     def ensure_spawned(self):
279         """Ensure a connection to a correctly running U-Boot instance.
280
281         This may require spawning a new Sandbox process or resetting target
282         hardware, as defined by the implementation sub-class.
283
284         This is an internal function and should not be called directly.
285
286         Args:
287             None.
288
289         Returns:
290             Nothing.
291         """
292
293         if self.p:
294             return
295         try:
296             self.log.start_section('Starting U-Boot')
297             self.at_prompt = False
298             self.p = self.get_spawn()
299             # Real targets can take a long time to scroll large amounts of
300             # text if LCD is enabled. This value may need tweaking in the
301             # future, possibly per-test to be optimal. This works for 'help'
302             # on board 'seaboard'.
303             if not self.config.gdbserver:
304                 self.p.timeout = 30000
305             self.p.logfile_read = self.logstream
306             bcfg = self.config.buildconfig
307             config_spl = bcfg.get('config_spl', 'n') == 'y'
308             config_spl_serial_support = bcfg.get('config_spl_serial_support',
309                                                  'n') == 'y'
310             env_spl_skipped = self.config.env.get('env__spl_skipped',
311                                                   False)
312             if config_spl and config_spl_serial_support and not env_spl_skipped:
313                 m = self.p.expect([pattern_u_boot_spl_signon] +
314                                   self.bad_patterns)
315                 if m != 0:
316                     raise Exception('Bad pattern found on console: ' +
317                                     self.bad_pattern_ids[m - 1])
318             m = self.p.expect([pattern_u_boot_main_signon] + self.bad_patterns)
319             if m != 0:
320                 raise Exception('Bad pattern found on console: ' +
321                                 self.bad_pattern_ids[m - 1])
322             self.u_boot_version_string = self.p.after
323             while True:
324                 m = self.p.expect([self.prompt_escaped,
325                     pattern_stop_autoboot_prompt] + self.bad_patterns)
326                 if m == 0:
327                     break
328                 if m == 1:
329                     self.p.send(' ')
330                     continue
331                 raise Exception('Bad pattern found on console: ' +
332                                 self.bad_pattern_ids[m - 2])
333             self.at_prompt = True
334             self.at_prompt_logevt = self.logstream.logfile.cur_evt
335         except Exception as ex:
336             self.log.error(str(ex))
337             self.cleanup_spawn()
338             raise
339         finally:
340             self.log.end_section('Starting U-Boot')
341
342     def cleanup_spawn(self):
343         """Shut down all interaction with the U-Boot instance.
344
345         This is used when an error is detected prior to re-establishing a
346         connection with a fresh U-Boot instance.
347
348         This is an internal function and should not be called directly.
349
350         Args:
351             None.
352
353         Returns:
354             Nothing.
355         """
356
357         try:
358             if self.p:
359                 self.p.close()
360         except:
361             pass
362         self.p = None
363
364     def validate_version_string_in_text(self, text):
365         """Assert that a command's output includes the U-Boot signon message.
366
367         This is primarily useful for validating the "version" command without
368         duplicating the signon text regex in a test function.
369
370         Args:
371             text: The command output text to check.
372
373         Returns:
374             Nothing. An exception is raised if the validation fails.
375         """
376
377         assert(self.u_boot_version_string in text)
378
379     def disable_check(self, check_type):
380         """Temporarily disable an error check of U-Boot's output.
381
382         Create a new context manager (for use with the "with" statement) which
383         temporarily disables a particular console output error check.
384
385         Args:
386             check_type: The type of error-check to disable. Valid values may
387             be found in self.disable_check_count above.
388
389         Returns:
390             A context manager object.
391         """
392
393         return ConsoleDisableCheck(self, check_type)