xref: /rk3399_rockchip-uboot/test/py/u_boot_console_base.py (revision 636f38d83a7e0e6ca076ae65e086c800337fb3a3)
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
17
18# Regexes for text we expect U-Boot to send to the console.
19pattern_u_boot_spl_signon = re.compile('(U-Boot SPL \\d{4}\\.\\d{2}-[^\r\n]*)')
20pattern_u_boot_main_signon = re.compile('(U-Boot \\d{4}\\.\\d{2}-[^\r\n]*)')
21pattern_stop_autoboot_prompt = re.compile('Hit any key to stop autoboot: ')
22pattern_unknown_command = re.compile('Unknown command \'.*\' - try \'help\'')
23pattern_error_notification = re.compile('## Error: ')
24
25class 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
43class 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
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
359