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 which(tool): 21 """ looks up the full path for @tool, returns None if not found 22 or if @tool does not have executable permissions. 23 """ 24 paths=os.getenv('PATH') 25 for p in paths.split(os.path.pathsep): 26 p = os.path.join(p, tool) 27 if os.access(p, os.X_OK): 28 return p 29 return None 30 31def run_cmd(args): 32 subp = subprocess.Popen(args, 33 stdout=subprocess.PIPE, 34 stderr=subprocess.PIPE, 35 universal_newlines=True) 36 stdout, stderr = subp.communicate() 37 ret = subp.returncode 38 39 return (stdout, stderr, ret) 40 41def is_readable_executable_file(path): 42 return os.path.isfile(path) and os.access(path, os.R_OK | os.X_OK) 43 44# @test: functional test to fail if @failure is seen 45# @vm: the VM whose console to process 46# @success: a non-None string to look for 47# @failure: a string to look for that triggers test failure, or None 48# 49# Read up to 1 line of text from @vm, looking for @success 50# and optionally @failure. 51# 52# If @success or @failure are seen, immediately return True, 53# even if end of line is not yet seen. ie remainder of the 54# line is left unread. 55# 56# If end of line is seen, with neither @success or @failure 57# return False 58# 59# If @failure is seen, then mark @test as failed 60def _console_read_line_until_match(test, vm, success, failure): 61 msg = bytes([]) 62 done = False 63 while True: 64 c = vm.console_socket.recv(1) 65 if c is None: 66 done = True 67 test.fail( 68 f"EOF in console, expected '{success}'") 69 break 70 msg += c 71 72 if success in msg: 73 done = True 74 break 75 if failure and failure in msg: 76 done = True 77 vm.console_socket.close() 78 test.fail( 79 f"'{failure}' found in console, expected '{success}'") 80 81 if c == b'\n': 82 break 83 84 console_logger = logging.getLogger('console') 85 try: 86 console_logger.debug(msg.decode().strip()) 87 except: 88 console_logger.debug(msg) 89 90 return done 91 92def _console_interaction(test, success_message, failure_message, 93 send_string, keep_sending=False, vm=None): 94 assert not keep_sending or send_string 95 assert success_message or send_string 96 97 if vm is None: 98 vm = test.vm 99 100 test.log.debug( 101 f"Console interaction: success_msg='{success_message}' " + 102 f"failure_msg='{failure_message}' send_string='{send_string}'") 103 104 # We'll process console in bytes, to avoid having to 105 # deal with unicode decode errors from receiving 106 # partial utf8 byte sequences 107 success_message_b = None 108 if success_message is not None: 109 success_message_b = success_message.encode() 110 111 failure_message_b = None 112 if failure_message is not None: 113 failure_message_b = failure_message.encode() 114 115 while True: 116 if send_string: 117 vm.console_socket.sendall(send_string.encode()) 118 if not keep_sending: 119 send_string = None # send only once 120 121 # Only consume console output if waiting for something 122 if success_message is None: 123 if send_string is None: 124 break 125 continue 126 127 if _console_read_line_until_match(test, vm, 128 success_message_b, 129 failure_message_b): 130 break 131 132def interrupt_interactive_console_until_pattern(test, success_message, 133 failure_message=None, 134 interrupt_string='\r'): 135 """ 136 Keep sending a string to interrupt a console prompt, while logging the 137 console output. Typical use case is to break a boot loader prompt, such: 138 139 Press a key within 5 seconds to interrupt boot process. 140 5 141 4 142 3 143 2 144 1 145 Booting default image... 146 147 :param test: a test containing a VM that will have its console 148 read and probed for a success or failure message 149 :type test: :class:`qemu_test.QemuSystemTest` 150 :param success_message: if this message appears, test succeeds 151 :param failure_message: if this message appears, test fails 152 :param interrupt_string: a string to send to the console before trying 153 to read a new line 154 """ 155 assert success_message 156 _console_interaction(test, success_message, failure_message, 157 interrupt_string, True) 158 159def wait_for_console_pattern(test, success_message, failure_message=None, 160 vm=None): 161 """ 162 Waits for messages to appear on the console, while logging the content 163 164 :param test: a test containing a VM that will have its console 165 read and probed for a success or failure message 166 :type test: :class:`qemu_test.QemuSystemTest` 167 :param success_message: if this message appears, test succeeds 168 :param failure_message: if this message appears, test fails 169 """ 170 assert success_message 171 _console_interaction(test, success_message, failure_message, None, vm=vm) 172 173def exec_command(test, command): 174 """ 175 Send a command to a console (appending CRLF characters), while logging 176 the content. 177 178 :param test: a test containing a VM. 179 :type test: :class:`qemu_test.QemuSystemTest` 180 :param command: the command to send 181 :type command: str 182 """ 183 _console_interaction(test, None, None, command + '\r') 184 185def exec_command_and_wait_for_pattern(test, command, 186 success_message, failure_message=None): 187 """ 188 Send a command to a console (appending CRLF characters), then wait 189 for success_message to appear on the console, while logging the. 190 content. Mark the test as failed if failure_message is found instead. 191 192 :param test: a test containing a VM that will have its console 193 read and probed for a success or failure message 194 :type test: :class:`qemu_test.QemuSystemTest` 195 :param command: the command to send 196 :param success_message: if this message appears, test succeeds 197 :param failure_message: if this message appears, test fails 198 """ 199 assert success_message 200 _console_interaction(test, success_message, failure_message, command + '\r') 201 202def get_qemu_img(test): 203 test.log.debug('Looking for and selecting a qemu-img binary') 204 205 # If qemu-img has been built, use it, otherwise the system wide one 206 # will be used. 207 qemu_img = test.build_file('qemu-img') 208 if os.path.exists(qemu_img): 209 return qemu_img 210 qemu_img = which('qemu-img') 211 if qemu_img is not None: 212 return qemu_img 213 test.skipTest(f"qemu-img not found in build dir or '$PATH'") 214