xref: /openbmc/qemu/tests/functional/qemu_test/cmd.py (revision 05caa062)
1# Test class and utilities for functional tests
2#
3# Copyright 2018, 2024 Red Hat, Inc.
4#
5# Original Author (Avocado-based tests):
6#  Cleber Rosa <crosa@redhat.com>
7#
8# Adaption for standalone version:
9#  Thomas Huth <thuth@redhat.com>
10#
11# This work is licensed under the terms of the GNU GPL, version 2 or
12# later.  See the COPYING file in the top-level directory.
13
14import logging
15import os
16import os.path
17import subprocess
18
19
20def has_cmd(name, args=None):
21    """
22    This function is for use in a @skipUnless decorator, e.g.:
23
24        @skipUnless(*has_cmd('sudo -n', ('sudo', '-n', 'true')))
25        def test_something_that_needs_sudo(self):
26            ...
27    """
28
29    if args is None:
30        args = ('which', name)
31
32    try:
33        _, stderr, exitcode = run_cmd(args)
34    except Exception as e:
35        exitcode = -1
36        stderr = str(e)
37
38    if exitcode != 0:
39        cmd_line = ' '.join(args)
40        err = f'{name} required, but "{cmd_line}" failed: {stderr.strip()}'
41        return (False, err)
42    else:
43        return (True, '')
44
45def has_cmds(*cmds):
46    """
47    This function is for use in a @skipUnless decorator and
48    allows checking for the availability of multiple commands, e.g.:
49
50        @skipUnless(*has_cmds(('cmd1', ('cmd1', '--some-parameter')),
51                              'cmd2', 'cmd3'))
52        def test_something_that_needs_cmd1_and_cmd2(self):
53            ...
54    """
55
56    for cmd in cmds:
57        if isinstance(cmd, str):
58            cmd = (cmd,)
59
60        ok, errstr = has_cmd(*cmd)
61        if not ok:
62            return (False, errstr)
63
64    return (True, '')
65
66def run_cmd(args):
67    subp = subprocess.Popen(args,
68                            stdout=subprocess.PIPE,
69                            stderr=subprocess.PIPE,
70                            universal_newlines=True)
71    stdout, stderr = subp.communicate()
72    ret = subp.returncode
73
74    return (stdout, stderr, ret)
75
76def is_readable_executable_file(path):
77    return os.path.isfile(path) and os.access(path, os.R_OK | os.X_OK)
78
79def _console_interaction(test, success_message, failure_message,
80                         send_string, keep_sending=False, vm=None):
81    assert not keep_sending or send_string
82    if vm is None:
83        vm = test.vm
84    console = vm.console_file
85    console_logger = logging.getLogger('console')
86    while True:
87        if send_string:
88            vm.console_socket.sendall(send_string.encode())
89            if not keep_sending:
90                send_string = None # send only once
91
92        # Only consume console output if waiting for something
93        if success_message is None and failure_message is None:
94            if send_string is None:
95                break
96            continue
97
98        try:
99            msg = console.readline().decode().strip()
100        except UnicodeDecodeError:
101            msg = None
102        if not msg:
103            continue
104        console_logger.debug(msg)
105        if success_message is None or success_message in msg:
106            break
107        if failure_message and failure_message in msg:
108            console.close()
109            fail = 'Failure message found in console: "%s". Expected: "%s"' % \
110                    (failure_message, success_message)
111            test.fail(fail)
112
113def interrupt_interactive_console_until_pattern(test, success_message,
114                                                failure_message=None,
115                                                interrupt_string='\r'):
116    """
117    Keep sending a string to interrupt a console prompt, while logging the
118    console output. Typical use case is to break a boot loader prompt, such:
119
120        Press a key within 5 seconds to interrupt boot process.
121        5
122        4
123        3
124        2
125        1
126        Booting default image...
127
128    :param test: a  test containing a VM that will have its console
129                 read and probed for a success or failure message
130    :type test: :class:`qemu_test.QemuSystemTest`
131    :param success_message: if this message appears, test succeeds
132    :param failure_message: if this message appears, test fails
133    :param interrupt_string: a string to send to the console before trying
134                             to read a new line
135    """
136    _console_interaction(test, success_message, failure_message,
137                         interrupt_string, True)
138
139def wait_for_console_pattern(test, success_message, failure_message=None,
140                             vm=None):
141    """
142    Waits for messages to appear on the console, while logging the content
143
144    :param test: a test containing a VM that will have its console
145                 read and probed for a success or failure message
146    :type test: :class:`qemu_test.QemuSystemTest`
147    :param success_message: if this message appears, test succeeds
148    :param failure_message: if this message appears, test fails
149    """
150    _console_interaction(test, success_message, failure_message, None, vm=vm)
151
152def exec_command(test, command):
153    """
154    Send a command to a console (appending CRLF characters), while logging
155    the content.
156
157    :param test: a test containing a VM.
158    :type test: :class:`qemu_test.QemuSystemTest`
159    :param command: the command to send
160    :type command: str
161    """
162    _console_interaction(test, None, None, command + '\r')
163
164def exec_command_and_wait_for_pattern(test, command,
165                                      success_message, failure_message=None):
166    """
167    Send a command to a console (appending CRLF characters), then wait
168    for success_message to appear on the console, while logging the.
169    content. Mark the test as failed if failure_message is found instead.
170
171    :param test: a test containing a VM that will have its console
172                 read and probed for a success or failure message
173    :type test: :class:`qemu_test.QemuSystemTest`
174    :param command: the command to send
175    :param success_message: if this message appears, test succeeds
176    :param failure_message: if this message appears, test fails
177    """
178    _console_interaction(test, success_message, failure_message, command + '\r')
179