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.at_prompt = False
297            self.log.action('Starting U-Boot')
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            self.p.timeout = 30000
304            self.p.logfile_read = self.logstream
305            if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
306                m = self.p.expect([pattern_u_boot_spl_signon] + self.bad_patterns)
307                if m != 0:
308                    raise Exception('Bad pattern found on console: ' +
309                                    self.bad_pattern_ids[m - 1])
310            m = self.p.expect([pattern_u_boot_main_signon] + self.bad_patterns)
311            if m != 0:
312                raise Exception('Bad pattern found on console: ' +
313                                self.bad_pattern_ids[m - 1])
314            signon = self.p.after
315            build_idx = signon.find(', Build:')
316            if build_idx == -1:
317                self.u_boot_version_string = signon
318            else:
319                self.u_boot_version_string = signon[:build_idx]
320            while True:
321                m = self.p.expect([self.prompt_escaped,
322                    pattern_stop_autoboot_prompt] + self.bad_patterns)
323                if m == 0:
324                    break
325                if m == 1:
326                    self.p.send(chr(3)) # CTRL-C
327                    continue
328                raise Exception('Bad pattern found on console: ' +
329                                self.bad_pattern_ids[m - 2])
330            self.at_prompt = True
331            self.at_prompt_logevt = self.logstream.logfile.cur_evt
332        except Exception as ex:
333            self.log.error(str(ex))
334            self.cleanup_spawn()
335            raise
336
337    def cleanup_spawn(self):
338        """Shut down all interaction with the U-Boot instance.
339
340        This is used when an error is detected prior to re-establishing a
341        connection with a fresh U-Boot instance.
342
343        This is an internal function and should not be called directly.
344
345        Args:
346            None.
347
348        Returns:
349            Nothing.
350        """
351
352        try:
353            if self.p:
354                self.p.close()
355        except:
356            pass
357        self.p = None
358
359    def validate_version_string_in_text(self, text):
360        """Assert that a command's output includes the U-Boot signon message.
361
362        This is primarily useful for validating the "version" command without
363        duplicating the signon text regex in a test function.
364
365        Args:
366            text: The command output text to check.
367
368        Returns:
369            Nothing. An exception is raised if the validation fails.
370        """
371
372        assert(self.u_boot_version_string in text)
373
374    def disable_check(self, check_type):
375        """Temporarily disable an error check of U-Boot's output.
376
377        Create a new context manager (for use with the "with" statement) which
378        temporarily disables a particular console output error check.
379
380        Args:
381            check_type: The type of error-check to disable. Valid values may
382            be found in self.disable_check_count above.
383
384        Returns:
385            A context manager object.
386        """
387
388        return ConsoleDisableCheck(self, check_type)
389