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
17
18# Regexes for text we expect U-Boot to send to the console.
19pattern_u_boot_spl_signon = re.compile('(U-Boot SPL \\d{4}\\.\\d{2}-[^\r\n]*)')
20pattern_u_boot_main_signon = re.compile('(U-Boot \\d{4}\\.\\d{2}-[^\r\n]*)')
21pattern_stop_autoboot_prompt = re.compile('Hit any key to stop autoboot: ')
22pattern_unknown_command = re.compile('Unknown command \'.*\' - try \'help\'')
23pattern_error_notification = re.compile('## Error: ')
24
25class ConsoleDisableCheck(object):
26    '''Context manager (for Python's with statement) that temporarily disables
27    the specified console output error check. This is useful when deliberately
28    executing a command that is known to trigger one of the error checks, in
29    order to test that the error condition is actually raised. This class is
30    used internally by ConsoleBase::disable_check(); it is not intended for
31    direct usage.'''
32
33    def __init__(self, console, check_type):
34        self.console = console
35        self.check_type = check_type
36
37    def __enter__(self):
38        self.console.disable_check_count[self.check_type] += 1
39
40    def __exit__(self, extype, value, traceback):
41        self.console.disable_check_count[self.check_type] -= 1
42
43class ConsoleBase(object):
44    '''The interface through which test functions interact with the U-Boot
45    console. This primarily involves executing shell commands, capturing their
46    results, and checking for common error conditions. Some common utilities
47    are also provided too.'''
48
49    def __init__(self, log, config, max_fifo_fill):
50        '''Initialize a U-Boot console connection.
51
52        Can only usefully be called by sub-classes.
53
54        Args:
55            log: A mulptiplex_log.Logfile object, to which the U-Boot output
56                will be logged.
57            config: A configuration data structure, as built by conftest.py.
58            max_fifo_fill: The maximum number of characters to send to U-Boot
59                command-line before waiting for U-Boot to echo the characters
60                back. For UART-based HW without HW flow control, this value
61                should be set less than the UART RX FIFO size to avoid
62                overflow, assuming that U-Boot can't keep up with full-rate
63                traffic at the baud rate.
64
65        Returns:
66            Nothing.
67        '''
68
69        self.log = log
70        self.config = config
71        self.max_fifo_fill = max_fifo_fill
72
73        self.logstream = self.log.get_stream('console', sys.stdout)
74
75        # Array slice removes leading/trailing quotes
76        self.prompt = self.config.buildconfig['config_sys_prompt'][1:-1]
77        self.prompt_escaped = re.escape(self.prompt)
78        self.p = None
79        self.disable_check_count = {
80            'spl_signon': 0,
81            'main_signon': 0,
82            'unknown_command': 0,
83            'error_notification': 0,
84        }
85
86        self.at_prompt = False
87        self.at_prompt_logevt = None
88        self.ram_base = None
89
90    def close(self):
91        '''Terminate the connection to the U-Boot console.
92
93        This function is only useful once all interaction with U-Boot is
94        complete. Once this function is called, data cannot be sent to or
95        received from U-Boot.
96
97        Args:
98            None.
99
100        Returns:
101            Nothing.
102        '''
103
104        if self.p:
105            self.p.close()
106        self.logstream.close()
107
108    def run_command(self, cmd, wait_for_echo=True, send_nl=True,
109            wait_for_prompt=True):
110        '''Execute a command via the U-Boot console.
111
112        The command is always sent to U-Boot.
113
114        U-Boot echoes any command back to its output, and this function
115        typically waits for that to occur. The wait can be disabled by setting
116        wait_for_echo=False, which is useful e.g. when sending CTRL-C to
117        interrupt a long-running command such as "ums".
118
119        Command execution is typically triggered by sending a newline
120        character. This can be disabled by setting send_nl=False, which is
121        also useful when sending CTRL-C.
122
123        This function typically waits for the command to finish executing, and
124        returns the console output that it generated. This can be disabled by
125        setting wait_for_prompt=False, which is useful when invoking a long-
126        running command such as "ums".
127
128        Args:
129            cmd: The command to send.
130            wait_for_each: Boolean indicating whether to wait for U-Boot to
131                echo the command text back to its output.
132            send_nl: Boolean indicating whether to send a newline character
133                after the command string.
134            wait_for_prompt: Boolean indicating whether to wait for the
135                command prompt to be sent by U-Boot. This typically occurs
136                immediately after the command has been executed.
137
138        Returns:
139            If wait_for_prompt == False:
140                Nothing.
141            Else:
142                The output from U-Boot during command execution. In other
143                words, the text U-Boot emitted between the point it echod the
144                command string and emitted the subsequent command prompts.
145        '''
146
147        self.ensure_spawned()
148
149        if self.at_prompt and \
150                self.at_prompt_logevt != self.logstream.logfile.cur_evt:
151            self.logstream.write(self.prompt, implicit=True)
152
153        bad_patterns = []
154        bad_pattern_ids = []
155        if (self.disable_check_count['spl_signon'] == 0 and
156                self.u_boot_spl_signon):
157            bad_patterns.append(self.u_boot_spl_signon_escaped)
158            bad_pattern_ids.append('SPL signon')
159        if self.disable_check_count['main_signon'] == 0:
160            bad_patterns.append(self.u_boot_main_signon_escaped)
161            bad_pattern_ids.append('U-Boot main signon')
162        if self.disable_check_count['unknown_command'] == 0:
163            bad_patterns.append(pattern_unknown_command)
164            bad_pattern_ids.append('Unknown command')
165        if self.disable_check_count['error_notification'] == 0:
166            bad_patterns.append(pattern_error_notification)
167            bad_pattern_ids.append('Error notification')
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] + bad_patterns)
182                if m != 0:
183                    self.at_prompt = False
184                    raise Exception('Bad pattern found on console: ' +
185                                    bad_pattern_ids[m - 1])
186            if not wait_for_prompt:
187                return
188            m = self.p.expect([self.prompt_escaped] + bad_patterns)
189            if m != 0:
190                self.at_prompt = False
191                raise Exception('Bad pattern found on console: ' +
192                                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.run_command(chr(3), wait_for_echo=False, send_nl=False)
217
218    def ensure_spawned(self):
219        '''Ensure a connection to a correctly running U-Boot instance.
220
221        This may require spawning a new Sandbox process or resetting target
222        hardware, as defined by the implementation sub-class.
223
224        This is an internal function and should not be called directly.
225
226        Args:
227            None.
228
229        Returns:
230            Nothing.
231        '''
232
233        if self.p:
234            return
235        try:
236            self.at_prompt = False
237            self.log.action('Starting U-Boot')
238            self.p = self.get_spawn()
239            # Real targets can take a long time to scroll large amounts of
240            # text if LCD is enabled. This value may need tweaking in the
241            # future, possibly per-test to be optimal. This works for 'help'
242            # on board 'seaboard'.
243            self.p.timeout = 30000
244            self.p.logfile_read = self.logstream
245            if self.config.buildconfig.get('CONFIG_SPL', False) == 'y':
246                self.p.expect([pattern_u_boot_spl_signon])
247                self.u_boot_spl_signon = self.p.after
248                self.u_boot_spl_signon_escaped = re.escape(self.p.after)
249            else:
250                self.u_boot_spl_signon = None
251            self.p.expect([pattern_u_boot_main_signon])
252            self.u_boot_main_signon = self.p.after
253            self.u_boot_main_signon_escaped = re.escape(self.p.after)
254            build_idx = self.u_boot_main_signon.find(', Build:')
255            if build_idx == -1:
256                self.u_boot_version_string = self.u_boot_main_signon
257            else:
258                self.u_boot_version_string = self.u_boot_main_signon[:build_idx]
259            while True:
260                match = self.p.expect([self.prompt_escaped,
261                                       pattern_stop_autoboot_prompt])
262                if match == 1:
263                    self.p.send(chr(3)) # CTRL-C
264                    continue
265                break
266            self.at_prompt = True
267            self.at_prompt_logevt = self.logstream.logfile.cur_evt
268        except Exception as ex:
269            self.log.error(str(ex))
270            self.cleanup_spawn()
271            raise
272
273    def cleanup_spawn(self):
274        '''Shut down all interaction with the U-Boot instance.
275
276        This is used when an error is detected prior to re-establishing a
277        connection with a fresh U-Boot instance.
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        try:
289            if self.p:
290                self.p.close()
291        except:
292            pass
293        self.p = None
294
295    def validate_version_string_in_text(self, text):
296        '''Assert that a command's output includes the U-Boot signon message.
297
298        This is primarily useful for validating the "version" command without
299        duplicating the signon text regex in a test function.
300
301        Args:
302            text: The command output text to check.
303
304        Returns:
305            Nothing. An exception is raised if the validation fails.
306        '''
307
308        assert(self.u_boot_version_string in text)
309
310    def disable_check(self, check_type):
311        '''Temporarily disable an error check of U-Boot's output.
312
313        Create a new context manager (for use with the "with" statement) which
314        temporarily disables a particular console output error check.
315
316        Args:
317            check_type: The type of error-check to disable. Valid values may
318            be found in self.disable_check_count above.
319
320        Returns:
321            A context manager object.
322        '''
323
324        return ConsoleDisableCheck(self, check_type)
325
326    def find_ram_base(self):
327        '''Find the running U-Boot's RAM location.
328
329        Probe the running U-Boot to determine the address of the first bank
330        of RAM. This is useful for tests that test reading/writing RAM, or
331        load/save files that aren't associated with some standard address
332        typically represented in an environment variable such as
333        ${kernel_addr_r}. The value is cached so that it only needs to be
334        actively read once.
335
336        Args:
337            None.
338
339        Returns:
340            The address of U-Boot's first RAM bank, as an integer.
341        '''
342
343        if self.config.buildconfig.get('config_cmd_bdi', 'n') != 'y':
344            pytest.skip('bdinfo command not supported')
345        if self.ram_base == -1:
346            pytest.skip('Previously failed to find RAM bank start')
347        if self.ram_base is not None:
348            return self.ram_base
349
350        with self.log.section('find_ram_base'):
351            response = self.run_command('bdinfo')
352            for l in response.split('\n'):
353                if '-> start' in l:
354                    self.ram_base = int(l.split('=')[1].strip(), 16)
355                    break
356            if self.ram_base is None:
357                self.ram_base = -1
358                raise Exception('Failed to find RAM bank start in `bdinfo`')
359
360        return self.ram_base
361