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é 21fa32a634SThomas Huth 22fa32a634SThomas Huthdef has_cmd(name, args=None): 23fa32a634SThomas Huth """ 24fa32a634SThomas Huth This function is for use in a @skipUnless decorator, e.g.: 25fa32a634SThomas Huth 26fa32a634SThomas Huth @skipUnless(*has_cmd('sudo -n', ('sudo', '-n', 'true'))) 27fa32a634SThomas Huth def test_something_that_needs_sudo(self): 28fa32a634SThomas Huth ... 29fa32a634SThomas Huth """ 30fa32a634SThomas Huth 31fa32a634SThomas Huth if args is None: 32fa32a634SThomas Huth args = ('which', name) 33fa32a634SThomas Huth 34fa32a634SThomas Huth try: 35fa32a634SThomas Huth _, stderr, exitcode = run_cmd(args) 36fa32a634SThomas Huth except Exception as e: 37fa32a634SThomas Huth exitcode = -1 38fa32a634SThomas Huth stderr = str(e) 39fa32a634SThomas Huth 40fa32a634SThomas Huth if exitcode != 0: 41fa32a634SThomas Huth cmd_line = ' '.join(args) 42fa32a634SThomas Huth err = f'{name} required, but "{cmd_line}" failed: {stderr.strip()}' 43fa32a634SThomas Huth return (False, err) 44fa32a634SThomas Huth else: 45fa32a634SThomas Huth return (True, '') 46fa32a634SThomas Huth 47fa32a634SThomas Huthdef has_cmds(*cmds): 48fa32a634SThomas Huth """ 49fa32a634SThomas Huth This function is for use in a @skipUnless decorator and 50fa32a634SThomas Huth allows checking for the availability of multiple commands, e.g.: 51fa32a634SThomas Huth 52fa32a634SThomas Huth @skipUnless(*has_cmds(('cmd1', ('cmd1', '--some-parameter')), 53fa32a634SThomas Huth 'cmd2', 'cmd3')) 54fa32a634SThomas Huth def test_something_that_needs_cmd1_and_cmd2(self): 55fa32a634SThomas Huth ... 56fa32a634SThomas Huth """ 57fa32a634SThomas Huth 58fa32a634SThomas Huth for cmd in cmds: 59fa32a634SThomas Huth if isinstance(cmd, str): 60fa32a634SThomas Huth cmd = (cmd,) 61fa32a634SThomas Huth 62fa32a634SThomas Huth ok, errstr = has_cmd(*cmd) 63fa32a634SThomas Huth if not ok: 64fa32a634SThomas Huth return (False, errstr) 65fa32a634SThomas Huth 66fa32a634SThomas Huth return (True, '') 67fa32a634SThomas Huth 68fa32a634SThomas Huthdef run_cmd(args): 69fa32a634SThomas Huth subp = subprocess.Popen(args, 70fa32a634SThomas Huth stdout=subprocess.PIPE, 71fa32a634SThomas Huth stderr=subprocess.PIPE, 72fa32a634SThomas Huth universal_newlines=True) 73fa32a634SThomas Huth stdout, stderr = subp.communicate() 74fa32a634SThomas Huth ret = subp.returncode 75fa32a634SThomas Huth 76fa32a634SThomas Huth return (stdout, stderr, ret) 77fa32a634SThomas Huth 78fa32a634SThomas Huthdef is_readable_executable_file(path): 79fa32a634SThomas Huth return os.path.isfile(path) and os.access(path, os.R_OK | os.X_OK) 80fa32a634SThomas Huth 81fa32a634SThomas Huthdef _console_interaction(test, success_message, failure_message, 82fa32a634SThomas Huth send_string, keep_sending=False, vm=None): 83fa32a634SThomas Huth assert not keep_sending or send_string 84fa32a634SThomas Huth if vm is None: 85fa32a634SThomas Huth vm = test.vm 86fa32a634SThomas Huth console = vm.console_file 87fa32a634SThomas Huth console_logger = logging.getLogger('console') 88*6f0942b7SDaniel P. Berrangé test.log.debug( 89*6f0942b7SDaniel P. Berrangé f"Console interaction: success_msg='{success_message}' " + 90*6f0942b7SDaniel P. Berrangé f"failure_msg='{failure_message}' send_string='{send_string}'") 91fa32a634SThomas Huth while True: 92fa32a634SThomas Huth if send_string: 93fa32a634SThomas Huth vm.console_socket.sendall(send_string.encode()) 94fa32a634SThomas Huth if not keep_sending: 95fa32a634SThomas Huth send_string = None # send only once 96fa32a634SThomas Huth 97fa32a634SThomas Huth # Only consume console output if waiting for something 98fa32a634SThomas Huth if success_message is None and failure_message is None: 99fa32a634SThomas Huth if send_string is None: 100fa32a634SThomas Huth break 101fa32a634SThomas Huth continue 102fa32a634SThomas Huth 103fa32a634SThomas Huth try: 104fa32a634SThomas Huth msg = console.readline().decode().strip() 105fa32a634SThomas Huth except UnicodeDecodeError: 106fa32a634SThomas Huth msg = None 107fa32a634SThomas Huth if not msg: 108fa32a634SThomas Huth continue 109fa32a634SThomas Huth console_logger.debug(msg) 110fa32a634SThomas Huth if success_message is None or success_message in msg: 111fa32a634SThomas Huth break 112fa32a634SThomas Huth if failure_message and failure_message in msg: 113fa32a634SThomas Huth console.close() 114fa32a634SThomas Huth fail = 'Failure message found in console: "%s". Expected: "%s"' % \ 115fa32a634SThomas Huth (failure_message, success_message) 116fa32a634SThomas Huth test.fail(fail) 117fa32a634SThomas Huth 118fa32a634SThomas Huthdef interrupt_interactive_console_until_pattern(test, success_message, 119fa32a634SThomas Huth failure_message=None, 120fa32a634SThomas Huth interrupt_string='\r'): 121fa32a634SThomas Huth """ 122fa32a634SThomas Huth Keep sending a string to interrupt a console prompt, while logging the 123fa32a634SThomas Huth console output. Typical use case is to break a boot loader prompt, such: 124fa32a634SThomas Huth 125fa32a634SThomas Huth Press a key within 5 seconds to interrupt boot process. 126fa32a634SThomas Huth 5 127fa32a634SThomas Huth 4 128fa32a634SThomas Huth 3 129fa32a634SThomas Huth 2 130fa32a634SThomas Huth 1 131fa32a634SThomas Huth Booting default image... 132fa32a634SThomas Huth 133fa32a634SThomas Huth :param test: a test containing a VM that will have its console 134fa32a634SThomas Huth read and probed for a success or failure message 135fa32a634SThomas Huth :type test: :class:`qemu_test.QemuSystemTest` 136fa32a634SThomas Huth :param success_message: if this message appears, test succeeds 137fa32a634SThomas Huth :param failure_message: if this message appears, test fails 138fa32a634SThomas Huth :param interrupt_string: a string to send to the console before trying 139fa32a634SThomas Huth to read a new line 140fa32a634SThomas Huth """ 141fa32a634SThomas Huth _console_interaction(test, success_message, failure_message, 142fa32a634SThomas Huth interrupt_string, True) 143fa32a634SThomas Huth 144fa32a634SThomas Huthdef wait_for_console_pattern(test, success_message, failure_message=None, 145fa32a634SThomas Huth vm=None): 146fa32a634SThomas Huth """ 147fa32a634SThomas Huth Waits for messages to appear on the console, while logging the content 148fa32a634SThomas Huth 149fa32a634SThomas Huth :param test: a test containing a VM that will have its console 150fa32a634SThomas Huth read and probed for a success or failure message 151fa32a634SThomas Huth :type test: :class:`qemu_test.QemuSystemTest` 152fa32a634SThomas Huth :param success_message: if this message appears, test succeeds 153fa32a634SThomas Huth :param failure_message: if this message appears, test fails 154fa32a634SThomas Huth """ 155fa32a634SThomas Huth _console_interaction(test, success_message, failure_message, None, vm=vm) 156fa32a634SThomas Huth 157fa32a634SThomas Huthdef exec_command(test, command): 158fa32a634SThomas Huth """ 159fa32a634SThomas Huth Send a command to a console (appending CRLF characters), while logging 160fa32a634SThomas Huth the content. 161fa32a634SThomas Huth 162fa32a634SThomas Huth :param test: a test containing a VM. 163fa32a634SThomas Huth :type test: :class:`qemu_test.QemuSystemTest` 164fa32a634SThomas Huth :param command: the command to send 165fa32a634SThomas Huth :type command: str 166fa32a634SThomas Huth """ 167fa32a634SThomas Huth _console_interaction(test, None, None, command + '\r') 168fa32a634SThomas Huth 169fa32a634SThomas Huthdef exec_command_and_wait_for_pattern(test, command, 170fa32a634SThomas Huth success_message, failure_message=None): 171fa32a634SThomas Huth """ 172fa32a634SThomas Huth Send a command to a console (appending CRLF characters), then wait 173fa32a634SThomas Huth for success_message to appear on the console, while logging the. 174fa32a634SThomas Huth content. Mark the test as failed if failure_message is found instead. 175fa32a634SThomas Huth 176fa32a634SThomas Huth :param test: a test containing a VM that will have its console 177fa32a634SThomas Huth read and probed for a success or failure message 178fa32a634SThomas Huth :type test: :class:`qemu_test.QemuSystemTest` 179fa32a634SThomas Huth :param command: the command to send 180fa32a634SThomas Huth :param success_message: if this message appears, test succeeds 181fa32a634SThomas Huth :param failure_message: if this message appears, test fails 182fa32a634SThomas Huth """ 183fa32a634SThomas Huth _console_interaction(test, success_message, failure_message, command + '\r') 1841255f5e4SPhilippe Mathieu-Daudé 1851255f5e4SPhilippe Mathieu-Daudédef get_qemu_img(test): 1861255f5e4SPhilippe Mathieu-Daudé test.log.debug('Looking for and selecting a qemu-img binary') 1871255f5e4SPhilippe Mathieu-Daudé 1881255f5e4SPhilippe Mathieu-Daudé # If qemu-img has been built, use it, otherwise the system wide one 1891255f5e4SPhilippe Mathieu-Daudé # will be used. 1901255f5e4SPhilippe Mathieu-Daudé qemu_img = os.path.join(BUILD_DIR, 'qemu-img') 1911255f5e4SPhilippe Mathieu-Daudé if os.path.exists(qemu_img): 1921255f5e4SPhilippe Mathieu-Daudé return qemu_img 19359d10024SThomas Huth (has_system_qemu_img, errmsg) = has_cmd('qemu-img') 19459d10024SThomas Huth if has_system_qemu_img: 1951255f5e4SPhilippe Mathieu-Daudé return 'qemu-img' 19659d10024SThomas Huth test.skipTest(errmsg) 197