#!/usr/bin/env python3 # Copyright (c) 2024 Intel Corporation # # SPDX-License-Identifier: Apache-2.0 """ Blackbox tests for twister's command line functions concerning addons to normal functions """ import importlib import mock import os import pkg_resources import pytest import re import shutil import subprocess import sys from conftest import ZEPHYR_BASE, TEST_DATA, sample_filename_mock, testsuite_filename_mock from twisterlib.testplan import TestPlan class TestAddon: @classmethod def setup_class(cls): apath = os.path.join(ZEPHYR_BASE, 'scripts', 'twister') cls.loader = importlib.machinery.SourceFileLoader('__main__', apath) cls.spec = importlib.util.spec_from_loader(cls.loader.name, cls.loader) cls.twister_module = importlib.util.module_from_spec(cls.spec) @classmethod def teardown_class(cls): pass @pytest.mark.parametrize( 'ubsan_flags, expected_exit_value', [ # No sanitiser, no problem ([], '0'), # Sanitiser catches a mistake, error is raised (['--enable-ubsan'], '1') ], ids=['no sanitiser', 'ubsan'] ) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_enable_ubsan(self, out_path, ubsan_flags, expected_exit_value): test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'tests', 'san', 'ubsan') args = ['-i', '--outdir', out_path, '-T', test_path] + \ ubsan_flags + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) assert str(sys_exit.value) == expected_exit_value @pytest.mark.parametrize( 'lsan_flags, expected_exit_value', [ # No sanitiser, no problem ([], '0'), # Sanitiser catches a mistake, error is raised (['--enable-asan', '--enable-lsan'], '1') ], ids=['no sanitiser', 'lsan'] ) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_enable_lsan(self, out_path, lsan_flags, expected_exit_value): test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'tests', 'san', 'lsan') args = ['-i', '--outdir', out_path, '-T', test_path] + \ lsan_flags + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) assert str(sys_exit.value) == expected_exit_value @pytest.mark.parametrize( 'asan_flags, expected_exit_value, expect_asan', [ # No sanitiser, no problem # Note that on some runs it may fail, # as the process is killed instead of ending normally. # This is not 100% repeatable, so this test is removed for now. # ([], '0', False), # Sanitiser catches a mistake, error is raised (['--enable-asan'], '1', True) ], ids=[ #'no sanitiser', 'asan' ] ) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_enable_asan(self, capfd, out_path, asan_flags, expected_exit_value, expect_asan): test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'tests', 'san', 'asan') args = ['-i', '-W', '--outdir', out_path, '-T', test_path] + \ asan_flags + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) assert str(sys_exit.value) == expected_exit_value out, err = capfd.readouterr() sys.stdout.write(out) sys.stderr.write(err) asan_template = r'^==\d+==ERROR:\s+AddressSanitizer:' assert expect_asan == bool(re.search(asan_template, err, re.MULTILINE)) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_extra_test_args(self, capfd, out_path): test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'tests', 'params', 'dummy') args = ['-i', '--outdir', out_path, '-T', test_path] + \ [] + \ ['-vvv'] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] + \ ['--', '-list'] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) # Use of -list makes tests not run. # Thus, the tests 'failed'. assert str(sys_exit.value) == '1' out, err = capfd.readouterr() sys.stdout.write(out) sys.stderr.write(err) expected_test_names = [ 'param_tests::test_assert1', 'param_tests::test_assert2', 'param_tests::test_assert3', ] assert all([testname in err for testname in expected_test_names]) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_extra_args(self, caplog, out_path): test_platforms = ['qemu_x86', 'intel_adl_crb'] path = os.path.join(TEST_DATA, 'tests', 'dummy', 'agnostic', 'group2') args = ['--outdir', out_path, '-T', path] + \ ['--extra-args', 'USE_CCACHE=0', '--extra-args', 'DUMMY=1'] + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) assert str(sys_exit.value) == '0' with open(os.path.join(out_path, 'twister.log')) as f: twister_log = f.read() pattern_cache = r'Calling cmake: [^\n]+ -DUSE_CCACHE=0 [^\n]+\n' pattern_dummy = r'Calling cmake: [^\n]+ -DDUMMY=1 [^\n]+\n' assert ' -DUSE_CCACHE=0 ' in twister_log res = re.search(pattern_cache, twister_log) assert res assert ' -DDUMMY=1 ' in twister_log res = re.search(pattern_dummy, twister_log) assert res # This test is not side-effect free. # It installs and uninstalls pytest-twister-harness using pip # It uses pip to check whether that plugin is previously installed # and reinstalls it if detected at the start of its run. # However, it does NOT restore the original plugin, ONLY reinstalls it. @pytest.mark.parametrize( 'allow_flags, do_install, expected_exit_value, expected_logs', [ ([], True, '1', ['By default Twister should work without pytest-twister-harness' ' plugin being installed, so please, uninstall it by' ' `pip uninstall pytest-twister-harness` and' ' `git clean -dxf scripts/pylib/pytest-twister-harness`.']), (['--allow-installed-plugin'], True, '0', ['You work with installed version' ' of pytest-twister-harness plugin.']), ([], False, '0', []), (['--allow-installed-plugin'], False, '0', []), ], ids=['installed, but not allowed', 'installed, allowed', 'not installed, not allowed', 'not installed, but allowed'] ) @mock.patch.object(TestPlan, 'SAMPLE_FILENAME', sample_filename_mock) def test_allow_installed_plugin(self, caplog, out_path, allow_flags, do_install, expected_exit_value, expected_logs): environment_twister_module = importlib.import_module('twisterlib.environment') harness_twister_module = importlib.import_module('twisterlib.harness') runner_twister_module = importlib.import_module('twisterlib.runner') pth_path = os.path.join(ZEPHYR_BASE, 'scripts', 'pylib', 'pytest-twister-harness') check_installed_command = [sys.executable, '-m', 'pip', 'list'] install_command = [sys.executable, '-m', 'pip', 'install', '--no-input', pth_path] uninstall_command = [sys.executable, '-m', 'pip', 'uninstall', '--yes', 'pytest-twister-harness'] def big_uninstall(): pth_path = os.path.join(ZEPHYR_BASE, 'scripts', 'pylib', 'pytest-twister-harness') subprocess.run(uninstall_command, check=True,) # For our registration to work, we have to delete the installation cache additional_cache_paths = [ # Plugin cache os.path.join(pth_path, 'src', 'pytest_twister_harness.egg-info'), # Additional caches os.path.join(pth_path, 'src', 'pytest_twister_harness', '__pycache__'), os.path.join(pth_path, 'src', 'pytest_twister_harness', 'device', '__pycache__'), os.path.join(pth_path, 'src', 'pytest_twister_harness', 'helpers', '__pycache__'), os.path.join(pth_path, 'src', 'pytest_twister_harness', 'build'), ] for additional_cache_path in additional_cache_paths: if os.path.exists(additional_cache_path): if os.path.isfile(additional_cache_path): os.unlink(additional_cache_path) else: shutil.rmtree(additional_cache_path) # To refresh the PYTEST_PLUGIN_INSTALLED global variable def refresh_plugin_installed_variable(): pkg_resources._initialize_master_working_set() importlib.reload(environment_twister_module) importlib.reload(harness_twister_module) importlib.reload(runner_twister_module) check_installed_result = subprocess.run(check_installed_command, check=True, capture_output=True, text=True) previously_installed = 'pytest-twister-harness' in check_installed_result.stdout # To ensure consistent test start big_uninstall() if do_install: subprocess.run(install_command, check=True) # Refresh before the test, no matter the testcase refresh_plugin_installed_variable() test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'samples', 'pytest', 'shell') args = ['-i', '--outdir', out_path, '-T', test_path] + \ allow_flags + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) # To ensure consistent test exit, prevent dehermetisation if do_install: big_uninstall() # To restore previously-installed plugin as well as we can if previously_installed: subprocess.run(install_command, check=True) if previously_installed or do_install: refresh_plugin_installed_variable() assert str(sys_exit.value) == expected_exit_value assert all([log in caplog.text for log in expected_logs]) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_pytest_args(self, out_path): test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'tests', 'pytest') args = ['-i', '--outdir', out_path, '-T', test_path] + \ ['--pytest-args=--custom-pytest-arg', '--pytest-args=foo', '--pytest-args=--cmdopt', '--pytest-args=.'] + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) # YAML was modified so that the test will fail without command line override. assert str(sys_exit.value) == '0' @pytest.mark.parametrize( 'valgrind_flags, expected_exit_value', [ # No sanitiser, leak is ignored ([], '0'), # Sanitiser catches a mistake, error is raised (['--enable-valgrind'], '1') ], ids=['no valgrind', 'valgrind'] ) @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) def test_enable_valgrind(self, capfd, out_path, valgrind_flags, expected_exit_value): test_platforms = ['native_sim'] test_path = os.path.join(TEST_DATA, 'tests', 'san', 'val') args = ['-i', '--outdir', out_path, '-T', test_path] + \ valgrind_flags + \ [] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] with mock.patch.object(sys, 'argv', [sys.argv[0]] + args), \ pytest.raises(SystemExit) as sys_exit: self.loader.exec_module(self.twister_module) assert str(sys_exit.value) == expected_exit_value