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