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