xref: /rk3399_rockchip-uboot/test/py/u_boot_console_base.py (revision 783cbcd3604088e9ff12b552fc209b3696c0e2b6)
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        self.ram_base = None
90
91    def close(self):
92        '''Terminate the connection to the U-Boot console.
93
94        This function is only useful once all interaction with U-Boot is
95        complete. Once this function is called, data cannot be sent to or
96        received from U-Boot.
97
98        Args:
99            None.
100
101        Returns:
102            Nothing.
103        '''
104
105        if self.p:
106            self.p.close()
107        self.logstream.close()
108
109    def run_command(self, cmd, wait_for_echo=True, send_nl=True,
110            wait_for_prompt=True):
111        '''Execute a command via the U-Boot console.
112
113        The command is always sent to U-Boot.
114
115        U-Boot echoes any command back to its output, and this function
116        typically waits for that to occur. The wait can be disabled by setting
117        wait_for_echo=False, which is useful e.g. when sending CTRL-C to
118        interrupt a long-running command such as "ums".
119
120        Command execution is typically triggered by sending a newline
121        character. This can be disabled by setting send_nl=False, which is
122        also useful when sending CTRL-C.
123
124        This function typically waits for the command to finish executing, and
125        returns the console output that it generated. This can be disabled by
126        setting wait_for_prompt=False, which is useful when invoking a long-
127        running command such as "ums".
128
129        Args:
130            cmd: The command to send.
131            wait_for_each: Boolean indicating whether to wait for U-Boot to
132                echo the command text back to its output.
133            send_nl: Boolean indicating whether to send a newline character
134                after the command string.
135            wait_for_prompt: Boolean indicating whether to wait for the
136                command prompt to be sent by U-Boot. This typically occurs
137                immediately after the command has been executed.
138
139        Returns:
140            If wait_for_prompt == False:
141                Nothing.
142            Else:
143                The output from U-Boot during command execution. In other
144                words, the text U-Boot emitted between the point it echod the
145                command string and emitted the subsequent command prompts.
146        '''
147
148        if self.at_prompt and \
149                self.at_prompt_logevt != self.logstream.logfile.cur_evt:
150            self.logstream.write(self.prompt, implicit=True)
151
152        bad_patterns = []
153        bad_pattern_ids = []
154        if (self.disable_check_count['spl_signon'] == 0 and
155                self.u_boot_spl_signon):
156            bad_patterns.append(self.u_boot_spl_signon_escaped)
157            bad_pattern_ids.append('SPL signon')
158        if self.disable_check_count['main_signon'] == 0:
159            bad_patterns.append(self.u_boot_main_signon_escaped)
160            bad_pattern_ids.append('U-Boot main signon')
161        if self.disable_check_count['unknown_command'] == 0:
162            bad_patterns.append(pattern_unknown_command)
163            bad_pattern_ids.append('Unknown command')
164        if self.disable_check_count['error_notification'] == 0:
165            bad_patterns.append(pattern_error_notification)
166            bad_pattern_ids.append('Error notification')
167        try:
168            self.at_prompt = False
169            if send_nl:
170                cmd += '\n'
171            while cmd:
172                # Limit max outstanding data, so UART FIFOs don't overflow
173                chunk = cmd[:self.max_fifo_fill]
174                cmd = cmd[self.max_fifo_fill:]
175                self.p.send(chunk)
176                if not wait_for_echo:
177                    continue
178                chunk = re.escape(chunk)
179                chunk = chunk.replace('\\\n', '[\r\n]')
180                m = self.p.expect([chunk] + bad_patterns)
181                if m != 0:
182                    self.at_prompt = False
183                    raise Exception('Bad pattern found on console: ' +
184                                    bad_pattern_ids[m - 1])
185            if not wait_for_prompt:
186                return
187            m = self.p.expect([self.prompt_escaped] + bad_patterns)
188            if m != 0:
189                self.at_prompt = False
190                raise Exception('Bad pattern found on console: ' +
191                                bad_pattern_ids[m - 1])
192            self.at_prompt = True
193            self.at_prompt_logevt = self.logstream.logfile.cur_evt
194            # Only strip \r\n; space/TAB might be significant if testing
195            # indentation.
196            return self.p.before.strip('\r\n')
197        except Exception as ex:
198            self.log.error(str(ex))
199            self.cleanup_spawn()
200            raise
201
202    def ctrlc(self):
203        '''Send a CTRL-C character to U-Boot.
204
205        This is useful in order to stop execution of long-running synchronous
206        commands such as "ums".
207
208        Args:
209            None.
210
211        Returns:
212            Nothing.
213        '''
214
215        self.log.action('Sending Ctrl-C')
216        self.run_command(chr(3), wait_for_echo=False, send_nl=False)
217
218    def drain_console(self):
219        '''Read from and log the U-Boot console for a short time.
220
221        U-Boot's console output is only logged when the test code actively
222        waits for U-Boot to emit specific data. There are cases where tests
223        can fail without doing this. For example, if a test asks U-Boot to
224        enable USB device mode, then polls until a host-side device node
225        exists. In such a case, it is useful to log U-Boot's console output
226        in case U-Boot printed clues as to why the host-side even did not
227        occur. This function will do that.
228
229        Args:
230            None.
231
232        Returns:
233            Nothing.
234        '''
235
236        # If we are already not connected to U-Boot, there's nothing to drain.
237        # This should only happen when a previous call to run_command() or
238        # wait_for() failed (and hence the output has already been logged), or
239        # the system is shutting down.
240        if not self.p:
241            return
242
243        orig_timeout = self.p.timeout
244        try:
245            # Drain the log for a relatively short time.
246            self.p.timeout = 1000
247            # Wait for something U-Boot will likely never send. This will
248            # cause the console output to be read and logged.
249            self.p.expect(['This should never match U-Boot output'])
250        except u_boot_spawn.Timeout:
251            pass
252        finally:
253            self.p.timeout = orig_timeout
254
255    def ensure_spawned(self):
256        '''Ensure a connection to a correctly running U-Boot instance.
257
258        This may require spawning a new Sandbox process or resetting target
259        hardware, as defined by the implementation sub-class.
260
261        This is an internal function and should not be called directly.
262
263        Args:
264            None.
265
266        Returns:
267            Nothing.
268        '''
269
270        if self.p:
271            return
272        try:
273            self.at_prompt = False
274            self.log.action('Starting U-Boot')
275            self.p = self.get_spawn()
276            # Real targets can take a long time to scroll large amounts of
277            # text if LCD is enabled. This value may need tweaking in the
278            # future, possibly per-test to be optimal. This works for 'help'
279            # on board 'seaboard'.
280            self.p.timeout = 30000
281            self.p.logfile_read = self.logstream
282            if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
283                self.p.expect([pattern_u_boot_spl_signon])
284                self.u_boot_spl_signon = self.p.after
285                self.u_boot_spl_signon_escaped = re.escape(self.p.after)
286            else:
287                self.u_boot_spl_signon = None
288            self.p.expect([pattern_u_boot_main_signon])
289            self.u_boot_main_signon = self.p.after
290            self.u_boot_main_signon_escaped = re.escape(self.p.after)
291            build_idx = self.u_boot_main_signon.find(', Build:')
292            if build_idx == -1:
293                self.u_boot_version_string = self.u_boot_main_signon
294            else:
295                self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
296            while True:
297                match = self.p.expect([self.prompt_escaped,
298                                       pattern_stop_autoboot_prompt])
299                if match == 1:
300                    self.p.send(chr(3)) # CTRL-C
301                    continue
302                break
303            self.at_prompt = True
304            self.at_prompt_logevt = self.logstream.logfile.cur_evt
305        except Exception as ex:
306            self.log.error(str(ex))
307            self.cleanup_spawn()
308            raise
309
310    def cleanup_spawn(self):
311        '''Shut down all interaction with the U-Boot instance.
312
313        This is used when an error is detected prior to re-establishing a
314        connection with a fresh U-Boot instance.
315
316        This is an internal function and should not be called directly.
317
318        Args:
319            None.
320
321        Returns:
322            Nothing.
323        '''
324
325        try:
326            if self.p:
327                self.p.close()
328        except:
329            pass
330        self.p = None
331
332    def validate_version_string_in_text(self, text):
333        '''Assert that a command's output includes the U-Boot signon message.
334
335        This is primarily useful for validating the "version" command without
336        duplicating the signon text regex in a test function.
337
338        Args:
339            text: The command output text to check.
340
341        Returns:
342            Nothing. An exception is raised if the validation fails.
343        '''
344
345        assert(self.u_boot_version_string in text)
346
347    def disable_check(self, check_type):
348        '''Temporarily disable an error check of U-Boot's output.
349
350        Create a new context manager (for use with the "with" statement) which
351        temporarily disables a particular console output error check.
352
353        Args:
354            check_type: The type of error-check to disable. Valid values may
355            be found in self.disable_check_count above.
356
357        Returns:
358            A context manager object.
359        '''
360
361        return ConsoleDisableCheck(self, check_type)
362
363    def find_ram_base(self):
364        '''Find the running U-Boot's RAM location.
365
366        Probe the running U-Boot to determine the address of the first bank
367        of RAM. This is useful for tests that test reading/writing RAM, or
368        load/save files that aren't associated with some standard address
369        typically represented in an environment variable such as
370        ${kernel_addr_r}. The value is cached so that it only needs to be
371        actively read once.
372
373        Args:
374            None.
375
376        Returns:
377            The address of U-Boot's first RAM bank, as an integer.
378        '''
379
380        if self.config.buildconfig.get('config_cmd_bdi', 'n') != 'y':
381            pytest.skip('bdinfo command not supported')
382        if self.ram_base == -1:
383            pytest.skip('Previously failed to find RAM bank start')
384        if self.ram_base is not None:
385            return self.ram_base
386
387        with self.log.section('find_ram_base'):
388            response = self.run_command('bdinfo')
389            for l in response.split('\n'):
390                if '-> start' in l:
391                    self.ram_base = int(l.split('=')[1].strip(), 16)
392                    break
393            if self.ram_base is None:
394                self.ram_base = -1
395                raise Exception('Failed to find RAM bank start in `bdinfo`')
396
397        return self.ram_base
398