xref: /rk3399_rockchip-uboot/test/py/u_boot_console_base.py (revision 05266103349e8409f5fbd55197c75c7bb58f575d)
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 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.log.action('Sending Ctrl-C')
215        self.run_command(chr(3), wait_for_echo=False, send_nl=False)
216
217    def wait_for(self, text):
218        '''Wait for a pattern to be emitted by U-Boot.
219
220        This is useful when a long-running command such as "dfu" is executing,
221        and it periodically emits some text that should show up at a specific
222        location in the log file.
223
224        Args:
225            text: The text to wait for; either a string (containing raw text,
226                not a regular expression) or an re object.
227
228        Returns:
229            Nothing.
230        '''
231
232        if type(text) == type(''):
233            text = re.escape(text)
234        self.p.expect([text])
235
236    def drain_console(self):
237        '''Read from and log the U-Boot console for a short time.
238
239        U-Boot's console output is only logged when the test code actively
240        waits for U-Boot to emit specific data. There are cases where tests
241        can fail without doing this. For example, if a test asks U-Boot to
242        enable USB device mode, then polls until a host-side device node
243        exists. In such a case, it is useful to log U-Boot's console output
244        in case U-Boot printed clues as to why the host-side even did not
245        occur. This function will do that.
246
247        Args:
248            None.
249
250        Returns:
251            Nothing.
252        '''
253
254        # If we are already not connected to U-Boot, there's nothing to drain.
255        # This should only happen when a previous call to run_command() or
256        # wait_for() failed (and hence the output has already been logged), or
257        # the system is shutting down.
258        if not self.p:
259            return
260
261        orig_timeout = self.p.timeout
262        try:
263            # Drain the log for a relatively short time.
264            self.p.timeout = 1000
265            # Wait for something U-Boot will likely never send. This will
266            # cause the console output to be read and logged.
267            self.p.expect(['This should never match U-Boot output'])
268        except u_boot_spawn.Timeout:
269            pass
270        finally:
271            self.p.timeout = orig_timeout
272
273    def ensure_spawned(self):
274        '''Ensure a connection to a correctly running U-Boot instance.
275
276        This may require spawning a new Sandbox process or resetting target
277        hardware, as defined by the implementation sub-class.
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        if self.p:
289            return
290        try:
291            self.at_prompt = False
292            self.log.action('Starting U-Boot')
293            self.p = self.get_spawn()
294            # Real targets can take a long time to scroll large amounts of
295            # text if LCD is enabled. This value may need tweaking in the
296            # future, possibly per-test to be optimal. This works for 'help'
297            # on board 'seaboard'.
298            self.p.timeout = 30000
299            self.p.logfile_read = self.logstream
300            if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
301                self.p.expect([pattern_u_boot_spl_signon])
302                self.u_boot_spl_signon = self.p.after
303                self.u_boot_spl_signon_escaped = re.escape(self.p.after)
304            else:
305                self.u_boot_spl_signon = None
306            self.p.expect([pattern_u_boot_main_signon])
307            self.u_boot_main_signon = self.p.after
308            self.u_boot_main_signon_escaped = re.escape(self.p.after)
309            build_idx = self.u_boot_main_signon.find(', Build:')
310            if build_idx == -1:
311                self.u_boot_version_string = self.u_boot_main_signon
312            else:
313                self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
314            while True:
315                match = self.p.expect([self.prompt_escaped,
316                                       pattern_stop_autoboot_prompt])
317                if match == 1:
318                    self.p.send(chr(3)) # CTRL-C
319                    continue
320                break
321            self.at_prompt = True
322            self.at_prompt_logevt = self.logstream.logfile.cur_evt
323        except Exception as ex:
324            self.log.error(str(ex))
325            self.cleanup_spawn()
326            raise
327
328    def cleanup_spawn(self):
329        '''Shut down all interaction with the U-Boot instance.
330
331        This is used when an error is detected prior to re-establishing a
332        connection with a fresh U-Boot instance.
333
334        This is an internal function and should not be called directly.
335
336        Args:
337            None.
338
339        Returns:
340            Nothing.
341        '''
342
343        try:
344            if self.p:
345                self.p.close()
346        except:
347            pass
348        self.p = None
349
350    def validate_version_string_in_text(self, text):
351        '''Assert that a command's output includes the U-Boot signon message.
352
353        This is primarily useful for validating the "version" command without
354        duplicating the signon text regex in a test function.
355
356        Args:
357            text: The command output text to check.
358
359        Returns:
360            Nothing. An exception is raised if the validation fails.
361        '''
362
363        assert(self.u_boot_version_string in text)
364
365    def disable_check(self, check_type):
366        '''Temporarily disable an error check of U-Boot's output.
367
368        Create a new context manager (for use with the "with" statement) which
369        temporarily disables a particular console output error check.
370
371        Args:
372            check_type: The type of error-check to disable. Valid values may
373            be found in self.disable_check_count above.
374
375        Returns:
376            A context manager object.
377        '''
378
379        return ConsoleDisableCheck(self, check_type)
380