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