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: ')
25pattern_error_please_reset = re.compile('### ERROR ### Please RESET the board ###')
26
27PAT_ID = 0
28PAT_RE = 1
29
30bad_pattern_defs = (
31    ('spl_signon', pattern_u_boot_spl_signon),
32    ('main_signon', pattern_u_boot_main_signon),
33    ('stop_autoboot_prompt', pattern_stop_autoboot_prompt),
34    ('unknown_command', pattern_unknown_command),
35    ('error_notification', pattern_error_notification),
36    ('error_please_reset', pattern_error_please_reset),
37)
38
39class ConsoleDisableCheck(object):
40    """Context manager (for Python's with statement) that temporarily disables
41    the specified console output error check. This is useful when deliberately
42    executing a command that is known to trigger one of the error checks, in
43    order to test that the error condition is actually raised. This class is
44    used internally by ConsoleBase::disable_check(); it is not intended for
45    direct usage."""
46
47    def __init__(self, console, check_type):
48        self.console = console
49        self.check_type = check_type
50
51    def __enter__(self):
52        self.console.disable_check_count[self.check_type] += 1
53        self.console.eval_bad_patterns()
54
55    def __exit__(self, extype, value, traceback):
56        self.console.disable_check_count[self.check_type] -= 1
57        self.console.eval_bad_patterns()
58
59class ConsoleBase(object):
60    """The interface through which test functions interact with the U-Boot
61    console. This primarily involves executing shell commands, capturing their
62    results, and checking for common error conditions. Some common utilities
63    are also provided too."""
64
65    def __init__(self, log, config, max_fifo_fill):
66        """Initialize a U-Boot console connection.
67
68        Can only usefully be called by sub-classes.
69
70        Args:
71            log: A mulptiplex_log.Logfile object, to which the U-Boot output
72                will be logged.
73            config: A configuration data structure, as built by conftest.py.
74            max_fifo_fill: The maximum number of characters to send to U-Boot
75                command-line before waiting for U-Boot to echo the characters
76                back. For UART-based HW without HW flow control, this value
77                should be set less than the UART RX FIFO size to avoid
78                overflow, assuming that U-Boot can't keep up with full-rate
79                traffic at the baud rate.
80
81        Returns:
82            Nothing.
83        """
84
85        self.log = log
86        self.config = config
87        self.max_fifo_fill = max_fifo_fill
88
89        self.logstream = self.log.get_stream('console', sys.stdout)
90
91        # Array slice removes leading/trailing quotes
92        self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
93        self.prompt_escaped = re.escape(self.prompt)
94        self.p = None
95        self.disable_check_count = {pat[PAT_ID]: 0 for pat in bad_pattern_defs}
96        self.eval_bad_patterns()
97
98        self.at_prompt = False
99        self.at_prompt_logevt = None
100
101    def eval_bad_patterns(self):
102        self.bad_patterns = [pat[PAT_RE] for pat in bad_pattern_defs \
103            if self.disable_check_count[pat[PAT_ID]] == 0]
104        self.bad_pattern_ids = [pat[PAT_ID] for pat in bad_pattern_defs \
105            if self.disable_check_count[pat[PAT_ID]] == 0]
106
107    def close(self):
108        """Terminate the connection to the U-Boot console.
109
110        This function is only useful once all interaction with U-Boot is
111        complete. Once this function is called, data cannot be sent to or
112        received from U-Boot.
113
114        Args:
115            None.
116
117        Returns:
118            Nothing.
119        """
120
121        if self.p:
122            self.p.close()
123        self.logstream.close()
124
125    def run_command(self, cmd, wait_for_echo=True, send_nl=True,
126            wait_for_prompt=True):
127        """Execute a command via the U-Boot console.
128
129        The command is always sent to U-Boot.
130
131        U-Boot echoes any command back to its output, and this function
132        typically waits for that to occur. The wait can be disabled by setting
133        wait_for_echo=False, which is useful e.g. when sending CTRL-C to
134        interrupt a long-running command such as "ums".
135
136        Command execution is typically triggered by sending a newline
137        character. This can be disabled by setting send_nl=False, which is
138        also useful when sending CTRL-C.
139
140        This function typically waits for the command to finish executing, and
141        returns the console output that it generated. This can be disabled by
142        setting wait_for_prompt=False, which is useful when invoking a long-
143        running command such as "ums".
144
145        Args:
146            cmd: The command to send.
147            wait_for_each: Boolean indicating whether to wait for U-Boot to
148                echo the command text back to its output.
149            send_nl: Boolean indicating whether to send a newline character
150                after the command string.
151            wait_for_prompt: Boolean indicating whether to wait for the
152                command prompt to be sent by U-Boot. This typically occurs
153                immediately after the command has been executed.
154
155        Returns:
156            If wait_for_prompt == False:
157                Nothing.
158            Else:
159                The output from U-Boot during command execution. In other
160                words, the text U-Boot emitted between the point it echod the
161                command string and emitted the subsequent command prompts.
162        """
163
164        if self.at_prompt and \
165                self.at_prompt_logevt != self.logstream.logfile.cur_evt:
166            self.logstream.write(self.prompt, implicit=True)
167
168        try:
169            self.at_prompt = False
170            if send_nl:
171                cmd += '\n'
172            while cmd:
173                # Limit max outstanding data, so UART FIFOs don't overflow
174                chunk = cmd[:self.max_fifo_fill]
175                cmd = cmd[self.max_fifo_fill:]
176                self.p.send(chunk)
177                if not wait_for_echo:
178                    continue
179                chunk = re.escape(chunk)
180                chunk = chunk.replace('\\\n', '[\r\n]')
181                m = self.p.expect([chunk] + self.bad_patterns)
182                if m != 0:
183                    self.at_prompt = False
184                    raise Exception('Bad pattern found on console: ' +
185                                    self.bad_pattern_ids[m - 1])
186            if not wait_for_prompt:
187                return
188            m = self.p.expect([self.prompt_escaped] + self.bad_patterns)
189            if m != 0:
190                self.at_prompt = False
191                raise Exception('Bad pattern found on console: ' +
192                                self.bad_pattern_ids[m - 1])
193            self.at_prompt = True
194            self.at_prompt_logevt = self.logstream.logfile.cur_evt
195            # Only strip \r\n; space/TAB might be significant if testing
196            # indentation.
197            return self.p.before.strip('\r\n')
198        except Exception as ex:
199            self.log.error(str(ex))
200            self.cleanup_spawn()
201            raise
202
203    def ctrlc(self):
204        """Send a CTRL-C character to U-Boot.
205
206        This is useful in order to stop execution of long-running synchronous
207        commands such as "ums".
208
209        Args:
210            None.
211
212        Returns:
213            Nothing.
214        """
215
216        self.log.action('Sending Ctrl-C')
217        self.run_command(chr(3), wait_for_echo=False, send_nl=False)
218
219    def wait_for(self, text):
220        """Wait for a pattern to be emitted by U-Boot.
221
222        This is useful when a long-running command such as "dfu" is executing,
223        and it periodically emits some text that should show up at a specific
224        location in the log file.
225
226        Args:
227            text: The text to wait for; either a string (containing raw text,
228                not a regular expression) or an re object.
229
230        Returns:
231            Nothing.
232        """
233
234        if type(text) == type(''):
235            text = re.escape(text)
236        m = self.p.expect([text] + self.bad_patterns)
237        if m != 0:
238            raise Exception('Bad pattern found on console: ' +
239                            self.bad_pattern_ids[m - 1])
240
241    def drain_console(self):
242        """Read from and log the U-Boot console for a short time.
243
244        U-Boot's console output is only logged when the test code actively
245        waits for U-Boot to emit specific data. There are cases where tests
246        can fail without doing this. For example, if a test asks U-Boot to
247        enable USB device mode, then polls until a host-side device node
248        exists. In such a case, it is useful to log U-Boot's console output
249        in case U-Boot printed clues as to why the host-side even did not
250        occur. This function will do that.
251
252        Args:
253            None.
254
255        Returns:
256            Nothing.
257        """
258
259        # If we are already not connected to U-Boot, there's nothing to drain.
260        # This should only happen when a previous call to run_command() or
261        # wait_for() failed (and hence the output has already been logged), or
262        # the system is shutting down.
263        if not self.p:
264            return
265
266        orig_timeout = self.p.timeout
267        try:
268            # Drain the log for a relatively short time.
269            self.p.timeout = 1000
270            # Wait for something U-Boot will likely never send. This will
271            # cause the console output to be read and logged.
272            self.p.expect(['This should never match U-Boot output'])
273        except u_boot_spawn.Timeout:
274            pass
275        finally:
276            self.p.timeout = orig_timeout
277
278    def ensure_spawned(self):
279        """Ensure a connection to a correctly running U-Boot instance.
280
281        This may require spawning a new Sandbox process or resetting target
282        hardware, as defined by the implementation sub-class.
283
284        This is an internal function and should not be called directly.
285
286        Args:
287            None.
288
289        Returns:
290            Nothing.
291        """
292
293        if self.p:
294            return
295        try:
296            self.log.start_section('Starting U-Boot')
297            self.at_prompt = False
298            self.p = self.get_spawn()
299            # Real targets can take a long time to scroll large amounts of
300            # text if LCD is enabled. This value may need tweaking in the
301            # future, possibly per-test to be optimal. This works for 'help'
302            # on board 'seaboard'.
303            if not self.config.gdbserver:
304                self.p.timeout = 30000
305            self.p.logfile_read = self.logstream
306            bcfg = self.config.buildconfig
307            config_spl = bcfg.get('config_spl', 'n') == 'y'
308            config_spl_serial_support = bcfg.get('config_spl_serial_support',
309                                                 'n') == 'y'
310            env_spl_skipped = self.config.env.get('env__spl_skipped',
311                                                  False)
312            if config_spl and config_spl_serial_support and not env_spl_skipped:
313                m = self.p.expect([pattern_u_boot_spl_signon] +
314                                  self.bad_patterns)
315                if m != 0:
316                    raise Exception('Bad pattern found on console: ' +
317                                    self.bad_pattern_ids[m - 1])
318            m = self.p.expect([pattern_u_boot_main_signon] + self.bad_patterns)
319            if m != 0:
320                raise Exception('Bad pattern found on console: ' +
321                                self.bad_pattern_ids[m - 1])
322            self.u_boot_version_string = self.p.after
323            while True:
324                m = self.p.expect([self.prompt_escaped,
325                    pattern_stop_autoboot_prompt] + self.bad_patterns)
326                if m == 0:
327                    break
328                if m == 1:
329                    self.p.send(' ')
330                    continue
331                raise Exception('Bad pattern found on console: ' +
332                                self.bad_pattern_ids[m - 2])
333            self.at_prompt = True
334            self.at_prompt_logevt = self.logstream.logfile.cur_evt
335        except Exception as ex:
336            self.log.error(str(ex))
337            self.cleanup_spawn()
338            raise
339        finally:
340            self.log.end_section('Starting U-Boot')
341
342    def cleanup_spawn(self):
343        """Shut down all interaction with the U-Boot instance.
344
345        This is used when an error is detected prior to re-establishing a
346        connection with a fresh U-Boot instance.
347
348        This is an internal function and should not be called directly.
349
350        Args:
351            None.
352
353        Returns:
354            Nothing.
355        """
356
357        try:
358            if self.p:
359                self.p.close()
360        except:
361            pass
362        self.p = None
363
364    def validate_version_string_in_text(self, text):
365        """Assert that a command's output includes the U-Boot signon message.
366
367        This is primarily useful for validating the "version" command without
368        duplicating the signon text regex in a test function.
369
370        Args:
371            text: The command output text to check.
372
373        Returns:
374            Nothing. An exception is raised if the validation fails.
375        """
376
377        assert(self.u_boot_version_string in text)
378
379    def disable_check(self, check_type):
380        """Temporarily disable an error check of U-Boot's output.
381
382        Create a new context manager (for use with the "with" statement) which
383        temporarily disables a particular console output error check.
384
385        Args:
386            check_type: The type of error-check to disable. Valid values may
387            be found in self.disable_check_count above.
388
389        Returns:
390            A context manager object.
391        """
392
393        return ConsoleDisableCheck(self, check_type)
394