]>
Commit | Line | Data |
---|---|---|
d201506c SW |
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 | ||
d201506c SW |
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.run_command(chr(3), wait_for_echo=False, send_nl=False) | |
215 | ||
216 | def ensure_spawned(self): | |
217 | '''Ensure a connection to a correctly running U-Boot instance. | |
218 | ||
219 | This may require spawning a new Sandbox process or resetting target | |
220 | hardware, as defined by the implementation sub-class. | |
221 | ||
222 | This is an internal function and should not be called directly. | |
223 | ||
224 | Args: | |
225 | None. | |
226 | ||
227 | Returns: | |
228 | Nothing. | |
229 | ''' | |
230 | ||
231 | if self.p: | |
232 | return | |
233 | try: | |
234 | self.at_prompt = False | |
235 | self.log.action('Starting U-Boot') | |
236 | self.p = self.get_spawn() | |
237 | # Real targets can take a long time to scroll large amounts of | |
238 | # text if LCD is enabled. This value may need tweaking in the | |
239 | # future, possibly per-test to be optimal. This works for 'help' | |
240 | # on board 'seaboard'. | |
241 | self.p.timeout = 30000 | |
242 | self.p.logfile_read = self.logstream | |
243 | if self.config.buildconfig.get('CONFIG_SPL', False) == 'y': | |
244 | self.p.expect([pattern_u_boot_spl_signon]) | |
245 | self.u_boot_spl_signon = self.p.after | |
246 | self.u_boot_spl_signon_escaped = re.escape(self.p.after) | |
247 | else: | |
248 | self.u_boot_spl_signon = None | |
249 | self.p.expect([pattern_u_boot_main_signon]) | |
250 | self.u_boot_main_signon = self.p.after | |
251 | self.u_boot_main_signon_escaped = re.escape(self.p.after) | |
252 | build_idx = self.u_boot_main_signon.find(', Build:') | |
253 | if build_idx == -1: | |
254 | self.u_boot_version_string = self.u_boot_main_signon | |
255 | else: | |
256 | self.u_boot_version_string = self.u_boot_main_signon[:build_idx] | |
257 | while True: | |
258 | match = self.p.expect([self.prompt_escaped, | |
259 | pattern_stop_autoboot_prompt]) | |
260 | if match == 1: | |
261 | self.p.send(chr(3)) # CTRL-C | |
262 | continue | |
263 | break | |
264 | self.at_prompt = True | |
265 | self.at_prompt_logevt = self.logstream.logfile.cur_evt | |
266 | except Exception as ex: | |
267 | self.log.error(str(ex)) | |
268 | self.cleanup_spawn() | |
269 | raise | |
270 | ||
271 | def cleanup_spawn(self): | |
272 | '''Shut down all interaction with the U-Boot instance. | |
273 | ||
274 | This is used when an error is detected prior to re-establishing a | |
275 | connection with a fresh U-Boot instance. | |
276 | ||
277 | This is an internal function and should not be called directly. | |
278 | ||
279 | Args: | |
280 | None. | |
281 | ||
282 | Returns: | |
283 | Nothing. | |
284 | ''' | |
285 | ||
286 | try: | |
287 | if self.p: | |
288 | self.p.close() | |
289 | except: | |
290 | pass | |
291 | self.p = None | |
292 | ||
293 | def validate_version_string_in_text(self, text): | |
294 | '''Assert that a command's output includes the U-Boot signon message. | |
295 | ||
296 | This is primarily useful for validating the "version" command without | |
297 | duplicating the signon text regex in a test function. | |
298 | ||
299 | Args: | |
300 | text: The command output text to check. | |
301 | ||
302 | Returns: | |
303 | Nothing. An exception is raised if the validation fails. | |
304 | ''' | |
305 | ||
306 | assert(self.u_boot_version_string in text) | |
307 | ||
308 | def disable_check(self, check_type): | |
309 | '''Temporarily disable an error check of U-Boot's output. | |
310 | ||
311 | Create a new context manager (for use with the "with" statement) which | |
312 | temporarily disables a particular console output error check. | |
313 | ||
314 | Args: | |
315 | check_type: The type of error-check to disable. Valid values may | |
316 | be found in self.disable_check_count above. | |
317 | ||
318 | Returns: | |
319 | A context manager object. | |
320 | ''' | |
321 | ||
322 | return ConsoleDisableCheck(self, check_type) | |
323 | ||
324 | def find_ram_base(self): | |
325 | '''Find the running U-Boot's RAM location. | |
326 | ||
327 | Probe the running U-Boot to determine the address of the first bank | |
328 | of RAM. This is useful for tests that test reading/writing RAM, or | |
329 | load/save files that aren't associated with some standard address | |
330 | typically represented in an environment variable such as | |
331 | ${kernel_addr_r}. The value is cached so that it only needs to be | |
332 | actively read once. | |
333 | ||
334 | Args: | |
335 | None. | |
336 | ||
337 | Returns: | |
338 | The address of U-Boot's first RAM bank, as an integer. | |
339 | ''' | |
340 | ||
341 | if self.config.buildconfig.get('config_cmd_bdi', 'n') != 'y': | |
342 | pytest.skip('bdinfo command not supported') | |
343 | if self.ram_base == -1: | |
344 | pytest.skip('Previously failed to find RAM bank start') | |
345 | if self.ram_base is not None: | |
346 | return self.ram_base | |
347 | ||
348 | with self.log.section('find_ram_base'): | |
349 | response = self.run_command('bdinfo') | |
350 | for l in response.split('\n'): | |
351 | if '-> start' in l: | |
352 | self.ram_base = int(l.split('=')[1].strip(), 16) | |
353 | break | |
354 | if self.ram_base is None: | |
355 | self.ram_base = -1 | |
356 | raise Exception('Failed to find RAM bank start in `bdinfo`') | |
357 | ||
358 | return self.ram_base |