xref: /rk3399_rockchip-uboot/test/py/u_boot_console_base.py (revision e4119ebb6dabf9bc84914b648f81c2f1b8fe76e1)
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
26PAT_ID = 0
27PAT_RE = 1
28
29bad_pattern_defs = (
30    ('spl_signon', pattern_u_boot_spl_signon),
31    ('main_signon', pattern_u_boot_main_signon),
32    ('stop_autoboot_prompt', pattern_stop_autoboot_prompt),
33    ('unknown_command', pattern_unknown_command),
34    ('error_notification', pattern_error_notification),
35)
36
37class ConsoleDisableCheck(object):
38    """Context manager (for Python's with statement) that temporarily disables
39    the specified console output error check. This is useful when deliberately
40    executing a command that is known to trigger one of the error checks, in
41    order to test that the error condition is actually raised. This class is
42    used internally by ConsoleBase::disable_check(); it is not intended for
43    direct usage."""
44
45    def __init__(self, console, check_type):
46        self.console = console
47        self.check_type = check_type
48
49    def __enter__(self):
50        self.console.disable_check_count[self.check_type] += 1
51        self.console.eval_bad_patterns()
52
53    def __exit__(self, extype, value, traceback):
54        self.console.disable_check_count[self.check_type] -= 1
55        self.console.eval_bad_patterns()
56
57class ConsoleBase(object):
58    """The interface through which test functions interact with the U-Boot
59    console. This primarily involves executing shell commands, capturing their
60    results, and checking for common error conditions. Some common utilities
61    are also provided too."""
62
63    def __init__(self, log, config, max_fifo_fill):
64        """Initialize a U-Boot console connection.
65
66        Can only usefully be called by sub-classes.
67
68        Args:
69            log: A mulptiplex_log.Logfile object, to which the U-Boot output
70                will be logged.
71            config: A configuration data structure, as built by conftest.py.
72            max_fifo_fill: The maximum number of characters to send to U-Boot
73                command-line before waiting for U-Boot to echo the characters
74                back. For UART-based HW without HW flow control, this value
75                should be set less than the UART RX FIFO size to avoid
76                overflow, assuming that U-Boot can't keep up with full-rate
77                traffic at the baud rate.
78
79        Returns:
80            Nothing.
81        """
82
83        self.log = log
84        self.config = config
85        self.max_fifo_fill = max_fifo_fill
86
87        self.logstream = self.log.get_stream('console', sys.stdout)
88
89        # Array slice removes leading/trailing quotes
90        self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
91        self.prompt_escaped = re.escape(self.prompt)
92        self.p = None
93        self.disable_check_count = {pat[PAT_ID]: 0 for pat in bad_pattern_defs}
94        self.eval_bad_patterns()
95
96        self.at_prompt = False
97        self.at_prompt_logevt = None
98
99    def eval_bad_patterns(self):
100        self.bad_patterns = [pat[PAT_RE] for pat in bad_pattern_defs \
101            if self.disable_check_count[pat[PAT_ID]] == 0]
102        self.bad_pattern_ids = [pat[PAT_ID] for pat in bad_pattern_defs \
103            if self.disable_check_count[pat[PAT_ID]] == 0]
104
105    def close(self):
106        """Terminate the connection to the U-Boot console.
107
108        This function is only useful once all interaction with U-Boot is
109        complete. Once this function is called, data cannot be sent to or
110        received from U-Boot.
111
112        Args:
113            None.
114
115        Returns:
116            Nothing.
117        """
118
119        if self.p:
120            self.p.close()
121        self.logstream.close()
122
123    def run_command(self, cmd, wait_for_echo=True, send_nl=True,
124            wait_for_prompt=True):
125        """Execute a command via the U-Boot console.
126
127        The command is always sent to U-Boot.
128
129        U-Boot echoes any command back to its output, and this function
130        typically waits for that to occur. The wait can be disabled by setting
131        wait_for_echo=False, which is useful e.g. when sending CTRL-C to
132        interrupt a long-running command such as "ums".
133
134        Command execution is typically triggered by sending a newline
135        character. This can be disabled by setting send_nl=False, which is
136        also useful when sending CTRL-C.
137
138        This function typically waits for the command to finish executing, and
139        returns the console output that it generated. This can be disabled by
140        setting wait_for_prompt=False, which is useful when invoking a long-
141        running command such as "ums".
142
143        Args:
144            cmd: The command to send.
145            wait_for_each: Boolean indicating whether to wait for U-Boot to
146                echo the command text back to its output.
147            send_nl: Boolean indicating whether to send a newline character
148                after the command string.
149            wait_for_prompt: Boolean indicating whether to wait for the
150                command prompt to be sent by U-Boot. This typically occurs
151                immediately after the command has been executed.
152
153        Returns:
154            If wait_for_prompt == False:
155                Nothing.
156            Else:
157                The output from U-Boot during command execution. In other
158                words, the text U-Boot emitted between the point it echod the
159                command string and emitted the subsequent command prompts.
160        """
161
162        if self.at_prompt and \
163                self.at_prompt_logevt != self.logstream.logfile.cur_evt:
164            self.logstream.write(self.prompt, implicit=True)
165
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] + self.bad_patterns)
180                if m != 0:
181                    self.at_prompt = False
182                    raise Exception('Bad pattern found on console: ' +
183                                    self.bad_pattern_ids[m - 1])
184            if not wait_for_prompt:
185                return
186            m = self.p.expect([self.prompt_escaped] + self.bad_patterns)
187            if m != 0:
188                self.at_prompt = False
189                raise Exception('Bad pattern found on console: ' +
190                                self.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.p.expect([pattern_u_boot_main_signon])
303            signon = self.p.after
304            build_idx = signon.find(', Build:')
305            if build_idx == -1:
306                self.u_boot_version_string = signon
307            else:
308                self.u_boot_version_string = signon[:build_idx]
309            while True:
310                match = self.p.expect([self.prompt_escaped,
311                                       pattern_stop_autoboot_prompt])
312                if match == 1:
313                    self.p.send(chr(3)) # CTRL-C
314                    continue
315                break
316            self.at_prompt = True
317            self.at_prompt_logevt = self.logstream.logfile.cur_evt
318        except Exception as ex:
319            self.log.error(str(ex))
320            self.cleanup_spawn()
321            raise
322
323    def cleanup_spawn(self):
324        """Shut down all interaction with the U-Boot instance.
325
326        This is used when an error is detected prior to re-establishing a
327        connection with a fresh U-Boot instance.
328
329        This is an internal function and should not be called directly.
330
331        Args:
332            None.
333
334        Returns:
335            Nothing.
336        """
337
338        try:
339            if self.p:
340                self.p.close()
341        except:
342            pass
343        self.p = None
344
345    def validate_version_string_in_text(self, text):
346        """Assert that a command's output includes the U-Boot signon message.
347
348        This is primarily useful for validating the "version" command without
349        duplicating the signon text regex in a test function.
350
351        Args:
352            text: The command output text to check.
353
354        Returns:
355            Nothing. An exception is raised if the validation fails.
356        """
357
358        assert(self.u_boot_version_string in text)
359
360    def disable_check(self, check_type):
361        """Temporarily disable an error check of U-Boot's output.
362
363        Create a new context manager (for use with the "with" statement) which
364        temporarily disables a particular console output error check.
365
366        Args:
367            check_type: The type of error-check to disable. Valid values may
368            be found in self.disable_check_count above.
369
370        Returns:
371            A context manager object.
372        """
373
374        return ConsoleDisableCheck(self, check_type)
375