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