xref: /openbmc/u-boot/test/py/u_boot_utils.py (revision abba76354a049c48fcdf4a252383ccd5c119b8ae)
176b46939SStephen Warren# SPDX-License-Identifier: GPL-2.0
283d290c5STom Rini# Copyright (c) 2016, NVIDIA CORPORATION. All rights reserved.
376b46939SStephen Warren
476b46939SStephen Warren# Utility code shared across multiple tests.
576b46939SStephen Warren
676b46939SStephen Warrenimport hashlib
7ac122efdSStephen Warrenimport inspect
876b46939SStephen Warrenimport os
976b46939SStephen Warrenimport os.path
10b8218a91SHeiko Schocherimport pytest
1176b46939SStephen Warrenimport sys
1276b46939SStephen Warrenimport time
13c3342cd5SLiam Beguinimport re
1476b46939SStephen Warren
1576b46939SStephen Warrendef md5sum_data(data):
16e8debf39SStephen Warren    """Calculate the MD5 hash of some data.
1776b46939SStephen Warren
1876b46939SStephen Warren    Args:
1976b46939SStephen Warren        data: The data to hash.
2076b46939SStephen Warren
2176b46939SStephen Warren    Returns:
2276b46939SStephen Warren        The hash of the data, as a binary string.
23e8debf39SStephen Warren    """
2476b46939SStephen Warren
2576b46939SStephen Warren    h = hashlib.md5()
2676b46939SStephen Warren    h.update(data)
2776b46939SStephen Warren    return h.digest()
2876b46939SStephen Warren
2976b46939SStephen Warrendef md5sum_file(fn, max_length=None):
30e8debf39SStephen Warren    """Calculate the MD5 hash of the contents of a file.
3176b46939SStephen Warren
3276b46939SStephen Warren    Args:
3376b46939SStephen Warren        fn: The filename of the file to hash.
3476b46939SStephen Warren        max_length: The number of bytes to hash. If the file has more
3576b46939SStephen Warren            bytes than this, they will be ignored. If None or omitted, the
3676b46939SStephen Warren            entire file will be hashed.
3776b46939SStephen Warren
3876b46939SStephen Warren    Returns:
3976b46939SStephen Warren        The hash of the file content, as a binary string.
40e8debf39SStephen Warren    """
4176b46939SStephen Warren
4276b46939SStephen Warren    with open(fn, 'rb') as fh:
4376b46939SStephen Warren        if max_length:
4476b46939SStephen Warren            params = [max_length]
4576b46939SStephen Warren        else:
4676b46939SStephen Warren            params = []
4776b46939SStephen Warren        data = fh.read(*params)
4876b46939SStephen Warren    return md5sum_data(data)
4976b46939SStephen Warren
5076b46939SStephen Warrenclass PersistentRandomFile(object):
51e8debf39SStephen Warren    """Generate and store information about a persistent file containing
52e8debf39SStephen Warren    random data."""
5376b46939SStephen Warren
5476b46939SStephen Warren    def __init__(self, u_boot_console, fn, size):
55e8debf39SStephen Warren        """Create or process the persistent file.
5676b46939SStephen Warren
5776b46939SStephen Warren        If the file does not exist, it is generated.
5876b46939SStephen Warren
5976b46939SStephen Warren        If the file does exist, its content is hashed for later comparison.
6076b46939SStephen Warren
6176b46939SStephen Warren        These files are always located in the "persistent data directory" of
6276b46939SStephen Warren        the current test run.
6376b46939SStephen Warren
6476b46939SStephen Warren        Args:
6576b46939SStephen Warren            u_boot_console: A console connection to U-Boot.
6676b46939SStephen Warren            fn: The filename (without path) to create.
6776b46939SStephen Warren            size: The desired size of the file in bytes.
6876b46939SStephen Warren
6976b46939SStephen Warren        Returns:
7076b46939SStephen Warren            Nothing.
71e8debf39SStephen Warren        """
7276b46939SStephen Warren
7376b46939SStephen Warren        self.fn = fn
7476b46939SStephen Warren
7576b46939SStephen Warren        self.abs_fn = u_boot_console.config.persistent_data_dir + '/' + fn
7676b46939SStephen Warren
7776b46939SStephen Warren        if os.path.exists(self.abs_fn):
7876b46939SStephen Warren            u_boot_console.log.action('Persistent data file ' + self.abs_fn +
7976b46939SStephen Warren                ' already exists')
8076b46939SStephen Warren            self.content_hash = md5sum_file(self.abs_fn)
8176b46939SStephen Warren        else:
8276b46939SStephen Warren            u_boot_console.log.action('Generating ' + self.abs_fn +
8376b46939SStephen Warren                ' (random, persistent, %d bytes)' % size)
8476b46939SStephen Warren            data = os.urandom(size)
8576b46939SStephen Warren            with open(self.abs_fn, 'wb') as fh:
8676b46939SStephen Warren                fh.write(data)
8776b46939SStephen Warren            self.content_hash = md5sum_data(data)
8876b46939SStephen Warren
8976b46939SStephen Warrendef attempt_to_open_file(fn):
90e8debf39SStephen Warren    """Attempt to open a file, without throwing exceptions.
9176b46939SStephen Warren
9276b46939SStephen Warren    Any errors (exceptions) that occur during the attempt to open the file
9376b46939SStephen Warren    are ignored. This is useful in order to test whether a file (in
9476b46939SStephen Warren    particular, a device node) exists and can be successfully opened, in order
9576b46939SStephen Warren    to poll for e.g. USB enumeration completion.
9676b46939SStephen Warren
9776b46939SStephen Warren    Args:
9876b46939SStephen Warren        fn: The filename to attempt to open.
9976b46939SStephen Warren
10076b46939SStephen Warren    Returns:
10176b46939SStephen Warren        An open file handle to the file, or None if the file could not be
10276b46939SStephen Warren            opened.
103e8debf39SStephen Warren    """
10476b46939SStephen Warren
10576b46939SStephen Warren    try:
10676b46939SStephen Warren        return open(fn, 'rb')
10776b46939SStephen Warren    except:
10876b46939SStephen Warren        return None
10976b46939SStephen Warren
11076b46939SStephen Warrendef wait_until_open_succeeds(fn):
111e8debf39SStephen Warren    """Poll until a file can be opened, or a timeout occurs.
11276b46939SStephen Warren
11376b46939SStephen Warren    Continually attempt to open a file, and return when this succeeds, or
11476b46939SStephen Warren    raise an exception after a timeout.
11576b46939SStephen Warren
11676b46939SStephen Warren    Args:
11776b46939SStephen Warren        fn: The filename to attempt to open.
11876b46939SStephen Warren
11976b46939SStephen Warren    Returns:
12076b46939SStephen Warren        An open file handle to the file.
121e8debf39SStephen Warren    """
12276b46939SStephen Warren
123b8c45550SPaul Burton    for i in range(100):
12476b46939SStephen Warren        fh = attempt_to_open_file(fn)
12576b46939SStephen Warren        if fh:
12676b46939SStephen Warren            return fh
12776b46939SStephen Warren        time.sleep(0.1)
12876b46939SStephen Warren    raise Exception('File could not be opened')
12976b46939SStephen Warren
13076b46939SStephen Warrendef wait_until_file_open_fails(fn, ignore_errors):
131e8debf39SStephen Warren    """Poll until a file cannot be opened, or a timeout occurs.
13276b46939SStephen Warren
13376b46939SStephen Warren    Continually attempt to open a file, and return when this fails, or
13476b46939SStephen Warren    raise an exception after a timeout.
13576b46939SStephen Warren
13676b46939SStephen Warren    Args:
13776b46939SStephen Warren        fn: The filename to attempt to open.
13876b46939SStephen Warren        ignore_errors: Indicate whether to ignore timeout errors. If True, the
13976b46939SStephen Warren            function will simply return if a timeout occurs, otherwise an
14076b46939SStephen Warren            exception will be raised.
14176b46939SStephen Warren
14276b46939SStephen Warren    Returns:
14376b46939SStephen Warren        Nothing.
144e8debf39SStephen Warren    """
14576b46939SStephen Warren
146b8c45550SPaul Burton    for i in range(100):
14776b46939SStephen Warren        fh = attempt_to_open_file(fn)
14876b46939SStephen Warren        if not fh:
14976b46939SStephen Warren            return
15076b46939SStephen Warren        fh.close()
15176b46939SStephen Warren        time.sleep(0.1)
15276b46939SStephen Warren    if ignore_errors:
15376b46939SStephen Warren        return
15476b46939SStephen Warren    raise Exception('File can still be opened')
15576b46939SStephen Warren
15676b46939SStephen Warrendef run_and_log(u_boot_console, cmd, ignore_errors=False):
157e8debf39SStephen Warren    """Run a command and log its output.
15876b46939SStephen Warren
15976b46939SStephen Warren    Args:
16076b46939SStephen Warren        u_boot_console: A console connection to U-Boot.
161ec70f8a9SSimon Glass        cmd: The command to run, as an array of argv[], or a string.
162ec70f8a9SSimon Glass            If a string, note that it is split up so that quoted spaces
163ec70f8a9SSimon Glass            will not be preserved. E.g. "fred and" becomes ['"fred', 'and"']
16476b46939SStephen Warren        ignore_errors: Indicate whether to ignore errors. If True, the function
16576b46939SStephen Warren            will simply return if the command cannot be executed or exits with
16676b46939SStephen Warren            an error code, otherwise an exception will be raised if such
16776b46939SStephen Warren            problems occur.
16876b46939SStephen Warren
16976b46939SStephen Warren    Returns:
170f3d3e95cSSimon Glass        The output as a string.
171e8debf39SStephen Warren    """
172ec70f8a9SSimon Glass    if isinstance(cmd, str):
173ec70f8a9SSimon Glass        cmd = cmd.split()
17476b46939SStephen Warren    runner = u_boot_console.log.get_runner(cmd[0], sys.stdout)
175f3d3e95cSSimon Glass    output = runner.run(cmd, ignore_errors=ignore_errors)
17676b46939SStephen Warren    runner.close()
177f3d3e95cSSimon Glass    return output
17805266103SStephen Warren
1799e17b034SSimon Glassdef run_and_log_expect_exception(u_boot_console, cmd, retcode, msg):
18072f52268SSimon Glass    """Run a command that is expected to fail.
1819e17b034SSimon Glass
1829e17b034SSimon Glass    This runs a command and checks that it fails with the expected return code
1839e17b034SSimon Glass    and exception method. If not, an exception is raised.
1849e17b034SSimon Glass
1859e17b034SSimon Glass    Args:
1869e17b034SSimon Glass        u_boot_console: A console connection to U-Boot.
1879e17b034SSimon Glass        cmd: The command to run, as an array of argv[].
1889e17b034SSimon Glass        retcode: Expected non-zero return code from the command.
18972f52268SSimon Glass        msg: String that should be contained within the command's output.
1909e17b034SSimon Glass    """
1919e17b034SSimon Glass    try:
1929e17b034SSimon Glass        runner = u_boot_console.log.get_runner(cmd[0], sys.stdout)
1939e17b034SSimon Glass        runner.run(cmd)
1949e17b034SSimon Glass    except Exception as e:
1957f64b187SSimon Glass        assert(retcode == runner.exit_status)
1969e17b034SSimon Glass        assert(msg in runner.output)
1979e17b034SSimon Glass    else:
1987f64b187SSimon Glass        raise Exception("Expected an exception with retcode %d message '%s',"
1997f64b187SSimon Glass                        "but it was not raised" % (retcode, msg))
2009e17b034SSimon Glass    finally:
2019e17b034SSimon Glass        runner.close()
2029e17b034SSimon Glass
20305266103SStephen Warrenram_base = None
20405266103SStephen Warrendef find_ram_base(u_boot_console):
205e8debf39SStephen Warren    """Find the running U-Boot's RAM location.
20605266103SStephen Warren
20705266103SStephen Warren    Probe the running U-Boot to determine the address of the first bank
20805266103SStephen Warren    of RAM. This is useful for tests that test reading/writing RAM, or
20905266103SStephen Warren    load/save files that aren't associated with some standard address
21005266103SStephen Warren    typically represented in an environment variable such as
21105266103SStephen Warren    ${kernel_addr_r}. The value is cached so that it only needs to be
21205266103SStephen Warren    actively read once.
21305266103SStephen Warren
21405266103SStephen Warren    Args:
21505266103SStephen Warren        u_boot_console: A console connection to U-Boot.
21605266103SStephen Warren
21705266103SStephen Warren    Returns:
21805266103SStephen Warren        The address of U-Boot's first RAM bank, as an integer.
219e8debf39SStephen Warren    """
22005266103SStephen Warren
22105266103SStephen Warren    global ram_base
22205266103SStephen Warren    if u_boot_console.config.buildconfig.get('config_cmd_bdi', 'n') != 'y':
22305266103SStephen Warren        pytest.skip('bdinfo command not supported')
22405266103SStephen Warren    if ram_base == -1:
22505266103SStephen Warren        pytest.skip('Previously failed to find RAM bank start')
22605266103SStephen Warren    if ram_base is not None:
22705266103SStephen Warren        return ram_base
22805266103SStephen Warren
22905266103SStephen Warren    with u_boot_console.log.section('find_ram_base'):
23005266103SStephen Warren        response = u_boot_console.run_command('bdinfo')
23105266103SStephen Warren        for l in response.split('\n'):
232d56dd0b1SDaniel Schwierzeck            if '-> start' in l or 'memstart    =' in l:
23305266103SStephen Warren                ram_base = int(l.split('=')[1].strip(), 16)
23405266103SStephen Warren                break
23505266103SStephen Warren        if ram_base is None:
23605266103SStephen Warren            ram_base = -1
23705266103SStephen Warren            raise Exception('Failed to find RAM bank start in `bdinfo`')
23805266103SStephen Warren
239*abba7635SQuentin Schulz    # We don't want ram_base to be zero as some functions test if the given
240*abba7635SQuentin Schulz    # address is NULL (0). Let's add 2MiB then (size of an ARM LPAE/v8 section).
241*abba7635SQuentin Schulz
242*abba7635SQuentin Schulz    if ram_base == 0:
243*abba7635SQuentin Schulz        ram_base += 1024 * 1024 * 2
244*abba7635SQuentin Schulz
24505266103SStephen Warren    return ram_base
246ac122efdSStephen Warren
247ac122efdSStephen Warrenclass PersistentFileHelperCtxMgr(object):
248ac122efdSStephen Warren    """A context manager for Python's "with" statement, which ensures that any
249ac122efdSStephen Warren    generated file is deleted (and hence regenerated) if its mtime is older
250ac122efdSStephen Warren    than the mtime of the Python module which generated it, and gets an mtime
251ac122efdSStephen Warren    newer than the mtime of the Python module which generated after it is
252ac122efdSStephen Warren    generated. Objects of this type should be created by factory function
253ac122efdSStephen Warren    persistent_file_helper rather than directly."""
254ac122efdSStephen Warren
255ac122efdSStephen Warren    def __init__(self, log, filename):
256ac122efdSStephen Warren        """Initialize a new object.
257ac122efdSStephen Warren
258ac122efdSStephen Warren        Args:
259ac122efdSStephen Warren            log: The Logfile object to log to.
260ac122efdSStephen Warren            filename: The filename of the generated file.
261ac122efdSStephen Warren
262ac122efdSStephen Warren        Returns:
263ac122efdSStephen Warren            Nothing.
264ac122efdSStephen Warren        """
265ac122efdSStephen Warren
266ac122efdSStephen Warren        self.log = log
267ac122efdSStephen Warren        self.filename = filename
268ac122efdSStephen Warren
269ac122efdSStephen Warren    def __enter__(self):
270ac122efdSStephen Warren        frame = inspect.stack()[1]
271ac122efdSStephen Warren        module = inspect.getmodule(frame[0])
272ac122efdSStephen Warren        self.module_filename = module.__file__
273ac122efdSStephen Warren        self.module_timestamp = os.path.getmtime(self.module_filename)
274ac122efdSStephen Warren
275ac122efdSStephen Warren        if os.path.exists(self.filename):
276ac122efdSStephen Warren            filename_timestamp = os.path.getmtime(self.filename)
277ac122efdSStephen Warren            if filename_timestamp < self.module_timestamp:
278ac122efdSStephen Warren                self.log.action('Removing stale generated file ' +
279ac122efdSStephen Warren                    self.filename)
280ac122efdSStephen Warren                os.unlink(self.filename)
281ac122efdSStephen Warren
282ac122efdSStephen Warren    def __exit__(self, extype, value, traceback):
283ac122efdSStephen Warren        if extype:
284ac122efdSStephen Warren            try:
285ac122efdSStephen Warren                os.path.unlink(self.filename)
286ac122efdSStephen Warren            except:
287ac122efdSStephen Warren                pass
288ac122efdSStephen Warren            return
289ac122efdSStephen Warren        logged = False
290ac122efdSStephen Warren        for i in range(20):
291ac122efdSStephen Warren            filename_timestamp = os.path.getmtime(self.filename)
292ac122efdSStephen Warren            if filename_timestamp > self.module_timestamp:
293ac122efdSStephen Warren                break
294ac122efdSStephen Warren            if not logged:
295ac122efdSStephen Warren                self.log.action(
296ac122efdSStephen Warren                    'Waiting for generated file timestamp to increase')
297ac122efdSStephen Warren                logged = True
298ac122efdSStephen Warren            os.utime(self.filename)
299ac122efdSStephen Warren            time.sleep(0.1)
300ac122efdSStephen Warren
301ac122efdSStephen Warrendef persistent_file_helper(u_boot_log, filename):
302ac122efdSStephen Warren    """Manage the timestamps and regeneration of a persistent generated
303ac122efdSStephen Warren    file. This function creates a context manager for Python's "with"
304ac122efdSStephen Warren    statement
305ac122efdSStephen Warren
306ac122efdSStephen Warren    Usage:
307ac122efdSStephen Warren        with persistent_file_helper(u_boot_console.log, filename):
308ac122efdSStephen Warren            code to generate the file, if it's missing.
309ac122efdSStephen Warren
310ac122efdSStephen Warren    Args:
311ac122efdSStephen Warren        u_boot_log: u_boot_console.log.
312ac122efdSStephen Warren        filename: The filename of the generated file.
313ac122efdSStephen Warren
314ac122efdSStephen Warren    Returns:
315ac122efdSStephen Warren        A context manager object.
316ac122efdSStephen Warren    """
317ac122efdSStephen Warren
318ac122efdSStephen Warren    return PersistentFileHelperCtxMgr(u_boot_log, filename)
319c3342cd5SLiam Beguin
320c3342cd5SLiam Beguindef crc32(u_boot_console, address, count):
321c3342cd5SLiam Beguin    """Helper function used to compute the CRC32 value of a section of RAM.
322c3342cd5SLiam Beguin
323c3342cd5SLiam Beguin    Args:
324c3342cd5SLiam Beguin        u_boot_console: A U-Boot console connection.
325c3342cd5SLiam Beguin        address: Address where data starts.
326c3342cd5SLiam Beguin        count: Amount of data to use for calculation.
327c3342cd5SLiam Beguin
328c3342cd5SLiam Beguin    Returns:
329c3342cd5SLiam Beguin        CRC32 value
330c3342cd5SLiam Beguin    """
331c3342cd5SLiam Beguin
332c3342cd5SLiam Beguin    bcfg = u_boot_console.config.buildconfig
333c3342cd5SLiam Beguin    has_cmd_crc32 = bcfg.get('config_cmd_crc32', 'n') == 'y'
334c3342cd5SLiam Beguin    assert has_cmd_crc32, 'Cannot compute crc32 without CONFIG_CMD_CRC32.'
335c3342cd5SLiam Beguin    output = u_boot_console.run_command('crc32 %08x %x' % (address, count))
336c3342cd5SLiam Beguin
337c3342cd5SLiam Beguin    m = re.search('==> ([0-9a-fA-F]{8})$', output)
338c3342cd5SLiam Beguin    assert m, 'CRC32 operation failed.'
339c3342cd5SLiam Beguin
340c3342cd5SLiam Beguin    return m.group(1)
341