xref: /rk3399_rockchip-uboot/test/py/u_boot_console_base.py (revision e787a58fe2544497bbc75066e0bc62868c7c4e65)
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
12import multiplexed_log
13import os
14import pytest
15import re
16import sys
17import u_boot_spawn
18
19# Regexes for text we expect U-Boot to send to the console.
20pattern_u_boot_spl_signon = re.compile('(U-Boot SPL \\d{4}\\.\\d{2}-[^\r\n]*)')
21pattern_u_boot_main_signon = re.compile('(U-Boot \\d{4}\\.\\d{2}-[^\r\n]*)')
22pattern_stop_autoboot_prompt = re.compile('Hit any key to stop autoboot: ')
23pattern_unknown_command = re.compile('Unknown command \'.*\' - try \'help\'')
24pattern_error_notification = re.compile('## Error: ')
25
26class 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
44class 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)
374