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