xref: /qemu/tests/functional/qemu_test/cmd.py (revision dd6dfc012c914d3e81be1da4b0ca7328949bdd08)
1fa32a634SThomas Huth# Test class and utilities for functional tests
2fa32a634SThomas Huth#
3fa32a634SThomas Huth# Copyright 2018, 2024 Red Hat, Inc.
4fa32a634SThomas Huth#
5fa32a634SThomas Huth# Original Author (Avocado-based tests):
6fa32a634SThomas Huth#  Cleber Rosa <crosa@redhat.com>
7fa32a634SThomas Huth#
8fa32a634SThomas Huth# Adaption for standalone version:
9fa32a634SThomas Huth#  Thomas Huth <thuth@redhat.com>
10fa32a634SThomas Huth#
11fa32a634SThomas Huth# This work is licensed under the terms of the GNU GPL, version 2 or
12fa32a634SThomas Huth# later.  See the COPYING file in the top-level directory.
13fa32a634SThomas Huth
14fa32a634SThomas Huthimport logging
15fa32a634SThomas Huthimport os
16fa32a634SThomas Huthimport os.path
17fa32a634SThomas Huthimport subprocess
18fa32a634SThomas Huth
191255f5e4SPhilippe Mathieu-Daudéfrom .config import BUILD_DIR
201255f5e4SPhilippe Mathieu-Daudé
210da341a7SDaniel P. Berrangédef which(tool):
220da341a7SDaniel P. Berrangé    """ looks up the full path for @tool, returns None if not found
230da341a7SDaniel P. Berrangé        or if @tool does not have executable permissions.
240da341a7SDaniel P. Berrangé    """
250da341a7SDaniel P. Berrangé    paths=os.getenv('PATH')
260da341a7SDaniel P. Berrangé    for p in paths.split(os.path.pathsep):
270da341a7SDaniel P. Berrangé        p = os.path.join(p, tool)
28*dd6dfc01SDaniel P. Berrangé        if os.access(p, os.X_OK):
290da341a7SDaniel P. Berrangé            return p
300da341a7SDaniel P. Berrangé    return None
31fa32a634SThomas Huth
32fa32a634SThomas Huthdef has_cmd(name, args=None):
33fa32a634SThomas Huth    """
34fa32a634SThomas Huth    This function is for use in a @skipUnless decorator, e.g.:
35fa32a634SThomas Huth
36fa32a634SThomas Huth        @skipUnless(*has_cmd('sudo -n', ('sudo', '-n', 'true')))
37fa32a634SThomas Huth        def test_something_that_needs_sudo(self):
38fa32a634SThomas Huth            ...
39fa32a634SThomas Huth    """
40fa32a634SThomas Huth
41fa32a634SThomas Huth    if args is None:
42fa32a634SThomas Huth        args = ('which', name)
43fa32a634SThomas Huth
44fa32a634SThomas Huth    try:
45fa32a634SThomas Huth        _, stderr, exitcode = run_cmd(args)
46fa32a634SThomas Huth    except Exception as e:
47fa32a634SThomas Huth        exitcode = -1
48fa32a634SThomas Huth        stderr = str(e)
49fa32a634SThomas Huth
50fa32a634SThomas Huth    if exitcode != 0:
51fa32a634SThomas Huth        cmd_line = ' '.join(args)
52fa32a634SThomas Huth        err = f'{name} required, but "{cmd_line}" failed: {stderr.strip()}'
53fa32a634SThomas Huth        return (False, err)
54fa32a634SThomas Huth    else:
55fa32a634SThomas Huth        return (True, '')
56fa32a634SThomas Huth
57fa32a634SThomas Huthdef has_cmds(*cmds):
58fa32a634SThomas Huth    """
59fa32a634SThomas Huth    This function is for use in a @skipUnless decorator and
60fa32a634SThomas Huth    allows checking for the availability of multiple commands, e.g.:
61fa32a634SThomas Huth
62fa32a634SThomas Huth        @skipUnless(*has_cmds(('cmd1', ('cmd1', '--some-parameter')),
63fa32a634SThomas Huth                              'cmd2', 'cmd3'))
64fa32a634SThomas Huth        def test_something_that_needs_cmd1_and_cmd2(self):
65fa32a634SThomas Huth            ...
66fa32a634SThomas Huth    """
67fa32a634SThomas Huth
68fa32a634SThomas Huth    for cmd in cmds:
69fa32a634SThomas Huth        if isinstance(cmd, str):
70fa32a634SThomas Huth            cmd = (cmd,)
71fa32a634SThomas Huth
72fa32a634SThomas Huth        ok, errstr = has_cmd(*cmd)
73fa32a634SThomas Huth        if not ok:
74fa32a634SThomas Huth            return (False, errstr)
75fa32a634SThomas Huth
76fa32a634SThomas Huth    return (True, '')
77fa32a634SThomas Huth
78fa32a634SThomas Huthdef run_cmd(args):
79fa32a634SThomas Huth    subp = subprocess.Popen(args,
80fa32a634SThomas Huth                            stdout=subprocess.PIPE,
81fa32a634SThomas Huth                            stderr=subprocess.PIPE,
82fa32a634SThomas Huth                            universal_newlines=True)
83fa32a634SThomas Huth    stdout, stderr = subp.communicate()
84fa32a634SThomas Huth    ret = subp.returncode
85fa32a634SThomas Huth
86fa32a634SThomas Huth    return (stdout, stderr, ret)
87fa32a634SThomas Huth
88fa32a634SThomas Huthdef is_readable_executable_file(path):
89fa32a634SThomas Huth    return os.path.isfile(path) and os.access(path, os.R_OK | os.X_OK)
90fa32a634SThomas Huth
91cdad03b7SDaniel P. Berrangé# @test: functional test to fail if @failure is seen
92cdad03b7SDaniel P. Berrangé# @vm: the VM whose console to process
93cdad03b7SDaniel P. Berrangé# @success: a non-None string to look for
94cdad03b7SDaniel P. Berrangé# @failure: a string to look for that triggers test failure, or None
95cdad03b7SDaniel P. Berrangé#
96cdad03b7SDaniel P. Berrangé# Read up to 1 line of text from @vm, looking for @success
97cdad03b7SDaniel P. Berrangé# and optionally @failure.
98cdad03b7SDaniel P. Berrangé#
99cdad03b7SDaniel P. Berrangé# If @success or @failure are seen, immediately return True,
100cdad03b7SDaniel P. Berrangé# even if end of line is not yet seen. ie remainder of the
101cdad03b7SDaniel P. Berrangé# line is left unread.
102cdad03b7SDaniel P. Berrangé#
103cdad03b7SDaniel P. Berrangé# If end of line is seen, with neither @success or @failure
104cdad03b7SDaniel P. Berrangé# return False
105cdad03b7SDaniel P. Berrangé#
106cdad03b7SDaniel P. Berrangé# If @failure is seen, then mark @test as failed
107cdad03b7SDaniel P. Berrangédef _console_read_line_until_match(test, vm, success, failure):
108cdad03b7SDaniel P. Berrangé    msg = bytes([])
109cdad03b7SDaniel P. Berrangé    done = False
110cdad03b7SDaniel P. Berrangé    while True:
111cdad03b7SDaniel P. Berrangé        c = vm.console_socket.recv(1)
112cdad03b7SDaniel P. Berrangé        if c is None:
113cdad03b7SDaniel P. Berrangé            done = True
114cdad03b7SDaniel P. Berrangé            test.fail(
115cdad03b7SDaniel P. Berrangé                f"EOF in console, expected '{success}'")
116cdad03b7SDaniel P. Berrangé            break
117cdad03b7SDaniel P. Berrangé        msg += c
118cdad03b7SDaniel P. Berrangé
119cdad03b7SDaniel P. Berrangé        if success in msg:
120cdad03b7SDaniel P. Berrangé            done = True
121cdad03b7SDaniel P. Berrangé            break
122cdad03b7SDaniel P. Berrangé        if failure and failure in msg:
123cdad03b7SDaniel P. Berrangé            done = True
124cdad03b7SDaniel P. Berrangé            vm.console_socket.close()
125cdad03b7SDaniel P. Berrangé            test.fail(
126cdad03b7SDaniel P. Berrangé                f"'{failure}' found in console, expected '{success}'")
127cdad03b7SDaniel P. Berrangé
128cdad03b7SDaniel P. Berrangé        if c == b'\n':
129cdad03b7SDaniel P. Berrangé            break
130cdad03b7SDaniel P. Berrangé
131cdad03b7SDaniel P. Berrangé    console_logger = logging.getLogger('console')
132cdad03b7SDaniel P. Berrangé    try:
133cdad03b7SDaniel P. Berrangé        console_logger.debug(msg.decode().strip())
134cdad03b7SDaniel P. Berrangé    except:
135cdad03b7SDaniel P. Berrangé        console_logger.debug(msg)
136cdad03b7SDaniel P. Berrangé
137cdad03b7SDaniel P. Berrangé    return done
138cdad03b7SDaniel P. Berrangé
139fa32a634SThomas Huthdef _console_interaction(test, success_message, failure_message,
140fa32a634SThomas Huth                         send_string, keep_sending=False, vm=None):
141fa32a634SThomas Huth    assert not keep_sending or send_string
142f03a8189SDaniel P. Berrangé    assert success_message or send_string
143f03a8189SDaniel P. Berrangé
144fa32a634SThomas Huth    if vm is None:
145fa32a634SThomas Huth        vm = test.vm
146cdad03b7SDaniel P. Berrangé
1476f0942b7SDaniel P. Berrangé    test.log.debug(
1486f0942b7SDaniel P. Berrangé        f"Console interaction: success_msg='{success_message}' " +
1496f0942b7SDaniel P. Berrangé        f"failure_msg='{failure_message}' send_string='{send_string}'")
150cdad03b7SDaniel P. Berrangé
151cdad03b7SDaniel P. Berrangé    # We'll process console in bytes, to avoid having to
152cdad03b7SDaniel P. Berrangé    # deal with unicode decode errors from receiving
153cdad03b7SDaniel P. Berrangé    # partial utf8 byte sequences
154cdad03b7SDaniel P. Berrangé    success_message_b = None
155cdad03b7SDaniel P. Berrangé    if success_message is not None:
156cdad03b7SDaniel P. Berrangé        success_message_b = success_message.encode()
157cdad03b7SDaniel P. Berrangé
158cdad03b7SDaniel P. Berrangé    failure_message_b = None
159cdad03b7SDaniel P. Berrangé    if failure_message is not None:
160cdad03b7SDaniel P. Berrangé        failure_message_b = failure_message.encode()
161cdad03b7SDaniel P. Berrangé
162fa32a634SThomas Huth    while True:
163fa32a634SThomas Huth        if send_string:
164fa32a634SThomas Huth            vm.console_socket.sendall(send_string.encode())
165fa32a634SThomas Huth            if not keep_sending:
166fa32a634SThomas Huth                send_string = None # send only once
167fa32a634SThomas Huth
168fa32a634SThomas Huth        # Only consume console output if waiting for something
169f03a8189SDaniel P. Berrangé        if success_message is None:
170fa32a634SThomas Huth            if send_string is None:
171fa32a634SThomas Huth                break
172fa32a634SThomas Huth            continue
173fa32a634SThomas Huth
174cdad03b7SDaniel P. Berrangé        if _console_read_line_until_match(test, vm,
175cdad03b7SDaniel P. Berrangé                                          success_message_b,
176cdad03b7SDaniel P. Berrangé                                          failure_message_b):
177fa32a634SThomas Huth            break
178fa32a634SThomas Huth
179fa32a634SThomas Huthdef interrupt_interactive_console_until_pattern(test, success_message,
180fa32a634SThomas Huth                                                failure_message=None,
181fa32a634SThomas Huth                                                interrupt_string='\r'):
182fa32a634SThomas Huth    """
183fa32a634SThomas Huth    Keep sending a string to interrupt a console prompt, while logging the
184fa32a634SThomas Huth    console output. Typical use case is to break a boot loader prompt, such:
185fa32a634SThomas Huth
186fa32a634SThomas Huth        Press a key within 5 seconds to interrupt boot process.
187fa32a634SThomas Huth        5
188fa32a634SThomas Huth        4
189fa32a634SThomas Huth        3
190fa32a634SThomas Huth        2
191fa32a634SThomas Huth        1
192fa32a634SThomas Huth        Booting default image...
193fa32a634SThomas Huth
194fa32a634SThomas Huth    :param test: a  test containing a VM that will have its console
195fa32a634SThomas Huth                 read and probed for a success or failure message
196fa32a634SThomas Huth    :type test: :class:`qemu_test.QemuSystemTest`
197fa32a634SThomas Huth    :param success_message: if this message appears, test succeeds
198fa32a634SThomas Huth    :param failure_message: if this message appears, test fails
199fa32a634SThomas Huth    :param interrupt_string: a string to send to the console before trying
200fa32a634SThomas Huth                             to read a new line
201fa32a634SThomas Huth    """
202f03a8189SDaniel P. Berrangé    assert success_message
203fa32a634SThomas Huth    _console_interaction(test, success_message, failure_message,
204fa32a634SThomas Huth                         interrupt_string, True)
205fa32a634SThomas Huth
206fa32a634SThomas Huthdef wait_for_console_pattern(test, success_message, failure_message=None,
207fa32a634SThomas Huth                             vm=None):
208fa32a634SThomas Huth    """
209fa32a634SThomas Huth    Waits for messages to appear on the console, while logging the content
210fa32a634SThomas Huth
211fa32a634SThomas Huth    :param test: a test containing a VM that will have its console
212fa32a634SThomas Huth                 read and probed for a success or failure message
213fa32a634SThomas Huth    :type test: :class:`qemu_test.QemuSystemTest`
214fa32a634SThomas Huth    :param success_message: if this message appears, test succeeds
215fa32a634SThomas Huth    :param failure_message: if this message appears, test fails
216fa32a634SThomas Huth    """
217f03a8189SDaniel P. Berrangé    assert success_message
218fa32a634SThomas Huth    _console_interaction(test, success_message, failure_message, None, vm=vm)
219fa32a634SThomas Huth
220fa32a634SThomas Huthdef exec_command(test, command):
221fa32a634SThomas Huth    """
222fa32a634SThomas Huth    Send a command to a console (appending CRLF characters), while logging
223fa32a634SThomas Huth    the content.
224fa32a634SThomas Huth
225fa32a634SThomas Huth    :param test: a test containing a VM.
226fa32a634SThomas Huth    :type test: :class:`qemu_test.QemuSystemTest`
227fa32a634SThomas Huth    :param command: the command to send
228fa32a634SThomas Huth    :type command: str
229fa32a634SThomas Huth    """
230fa32a634SThomas Huth    _console_interaction(test, None, None, command + '\r')
231fa32a634SThomas Huth
232fa32a634SThomas Huthdef exec_command_and_wait_for_pattern(test, command,
233fa32a634SThomas Huth                                      success_message, failure_message=None):
234fa32a634SThomas Huth    """
235fa32a634SThomas Huth    Send a command to a console (appending CRLF characters), then wait
236fa32a634SThomas Huth    for success_message to appear on the console, while logging the.
237fa32a634SThomas Huth    content. Mark the test as failed if failure_message is found instead.
238fa32a634SThomas Huth
239fa32a634SThomas Huth    :param test: a test containing a VM that will have its console
240fa32a634SThomas Huth                 read and probed for a success or failure message
241fa32a634SThomas Huth    :type test: :class:`qemu_test.QemuSystemTest`
242fa32a634SThomas Huth    :param command: the command to send
243fa32a634SThomas Huth    :param success_message: if this message appears, test succeeds
244fa32a634SThomas Huth    :param failure_message: if this message appears, test fails
245fa32a634SThomas Huth    """
246f03a8189SDaniel P. Berrangé    assert success_message
247fa32a634SThomas Huth    _console_interaction(test, success_message, failure_message, command + '\r')
2481255f5e4SPhilippe Mathieu-Daudé
2491255f5e4SPhilippe Mathieu-Daudédef get_qemu_img(test):
2501255f5e4SPhilippe Mathieu-Daudé    test.log.debug('Looking for and selecting a qemu-img binary')
2511255f5e4SPhilippe Mathieu-Daudé
2521255f5e4SPhilippe Mathieu-Daudé    # If qemu-img has been built, use it, otherwise the system wide one
2531255f5e4SPhilippe Mathieu-Daudé    # will be used.
2541255f5e4SPhilippe Mathieu-Daudé    qemu_img = os.path.join(BUILD_DIR, 'qemu-img')
2551255f5e4SPhilippe Mathieu-Daudé    if os.path.exists(qemu_img):
2561255f5e4SPhilippe Mathieu-Daudé        return qemu_img
25759d10024SThomas Huth    (has_system_qemu_img, errmsg) = has_cmd('qemu-img')
25859d10024SThomas Huth    if has_system_qemu_img:
2591255f5e4SPhilippe Mathieu-Daudé        return 'qemu-img'
26059d10024SThomas Huth    test.skipTest(errmsg)
261