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