#!/usr/bin/env python3 # Copyright (c) 2024 Intel Corporation # # SPDX-License-Identifier: Apache-2.0 """ Blackbox tests for twister's command line functions changing the output files. """ import importlib import re import mock import os import shutil import pytest import sys import tarfile # pylint: disable=no-name-in-module from conftest import ZEPHYR_BASE, TEST_DATA, sample_filename_mock, testsuite_filename_mock from twisterlib.testplan import TestPlan @mock.patch.object(TestPlan, 'TESTSUITE_FILENAME', testsuite_filename_mock) @mock.patch.object(TestPlan, 'SAMPLE_FILENAME', sample_filename_mock) class TestOutfile: @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( 'flag_section, clobber, expect_straggler', [ ([], True, False), (['--clobber-output'], False, False), (['--no-clean'], False, True), (['--clobber-output', '--no-clean'], False, True), ], ids=['clobber', 'do not clobber', 'do not clean', 'do not clobber, do not clean'] ) def test_clobber_output(self, out_path, flag_section, clobber, expect_straggler): test_platforms = ['qemu_x86', 'intel_adl_crb'] path = os.path.join(TEST_DATA, 'tests', 'dummy') args = ['-i', '--outdir', out_path, '-T', path, '-y'] + \ flag_section + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] # We create an empty 'blackbox-out' to trigger the clobbering os.mkdir(os.path.join(out_path)) # We want to have a single straggler to check for straggler_name = 'atavi.sm' straggler_path = os.path.join(out_path, straggler_name) open(straggler_path, 'a').close() 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' expected_dirs = ['blackbox-out'] if clobber: expected_dirs += ['blackbox-out.1'] current_dirs = os.listdir(os.path.normpath(os.path.join(out_path, '..'))) print(current_dirs) assert sorted(current_dirs) == sorted(expected_dirs) out_contents = os.listdir(os.path.join(out_path)) print(out_contents) if expect_straggler: assert straggler_name in out_contents else: assert straggler_name not in out_contents def test_runtime_artifact_cleanup(self, out_path): test_platforms = ['qemu_x86', 'intel_adl_crb'] path = os.path.join(TEST_DATA, 'samples', 'hello_world') args = ['-i', '--outdir', out_path, '-T', path] + \ ['--runtime-artifact-cleanup'] + \ [] + \ [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' relpath = os.path.relpath(path, ZEPHYR_BASE) sample_path = os.path.join(out_path, 'qemu_x86_atom', relpath, 'sample.basic.helloworld') listdir = os.listdir(sample_path) zephyr_listdir = os.listdir(os.path.join(sample_path, 'zephyr')) expected_contents = ['CMakeFiles', 'handler.log', 'build.ninja', 'CMakeCache.txt', 'zephyr', 'build.log'] expected_zephyr_contents = ['.config'] assert all([content in expected_zephyr_contents for content in zephyr_listdir]), \ 'Cleaned zephyr directory has unexpected files.' assert all([content in expected_contents for content in listdir]), \ 'Cleaned directory has unexpected files.' def test_short_build_path(self, out_path): test_platforms = ['qemu_x86'] path = os.path.join(TEST_DATA, 'tests', 'dummy', 'agnostic', 'group2') # twister_links dir does not exist in a dry run. args = ['-i', '--outdir', out_path, '-T', path] + \ ['--short-build-path'] + \ ['--ninja'] + \ [val for pair in zip( ['-p'] * len(test_platforms), test_platforms ) for val in pair] relative_test_path = os.path.relpath(path, ZEPHYR_BASE) test_result_path = os.path.join(out_path, 'qemu_x86_atom', relative_test_path, 'dummy.agnostic.group2') 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_running = r'Running\s+cmake\s+on\s+(?P[\\\/].*)\s+for\s+qemu_x86/atom\s*\n' res_running = re.search(pattern_running, twister_log) assert res_running # Spaces, forward slashes, etc. in the path as well as CMake peculiarities # require us to forgo simple RegExes. pattern_calling_line = r'Calling cmake: [^\n]+$' res_calling = re.search(pattern_calling_line, twister_log[res_running.end():], re.MULTILINE) calling_line = res_calling.group() # HIGHLY DANGEROUS pattern! # If the checked text is not CMake flags only, it is exponential! # Where N is the length of non-flag space-delimited text section. flag_pattern = r'(?:\S+(?: \\)?)+- ' cmake_path = shutil.which('cmake') if not cmake_path: assert False, 'CMake not found.' cmake_call_section = r'^Calling cmake: ' + re.escape(cmake_path) calling_line = re.sub(cmake_call_section, '', calling_line) calling_line = calling_line[::-1] flag_iterable = re.finditer(flag_pattern, calling_line) for match in flag_iterable: reversed_flag = match.group() flag = reversed_flag[::-1] # Build flag if flag.startswith(' -B'): flag_value = flag[3:] build_filename = os.path.basename(os.path.normpath(flag_value)) unshortened_build_path = os.path.join(test_result_path, build_filename) assert flag_value != unshortened_build_path, 'Build path unchanged.' assert len(flag_value) < len(unshortened_build_path), 'Build path not shortened.' # Pipe flag if flag.startswith(' -DQEMU_PIPE='): flag_value = flag[13:] pipe_filename = os.path.basename(os.path.normpath(flag_value)) unshortened_pipe_path = os.path.join(test_result_path, pipe_filename) assert flag_value != unshortened_pipe_path, 'Pipe path unchanged.' assert len(flag_value) < len(unshortened_pipe_path), 'Pipe path not shortened.' def test_prep_artifacts_for_testing(self, out_path): test_platforms = ['qemu_x86', 'intel_adl_crb'] path = os.path.join(TEST_DATA, 'samples', 'hello_world') relative_test_path = os.path.relpath(path, ZEPHYR_BASE) zephyr_out_path = os.path.join(out_path, 'qemu_x86_atom', relative_test_path, 'sample.basic.helloworld', 'zephyr') args = ['-i', '--outdir', out_path, '-T', path] + \ ['--prep-artifacts-for-testing'] + \ [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' zephyr_artifact_list = os.listdir(zephyr_out_path) # --build-only and normal run leave more files than --prep-artifacts-for-testing # However, the cost of testing that this leaves less seems to outweigh the benefits. # So we'll only check for the most important artifact. assert 'zephyr.elf' in zephyr_artifact_list def test_package_artifacts(self, out_path): test_platforms = ['qemu_x86'] path = os.path.join(TEST_DATA, 'samples', 'hello_world') package_name = 'PACKAGE' package_path = os.path.join(out_path, package_name) args = ['-i', '--outdir', out_path, '-T', path] + \ ['--package-artifacts', package_path] + \ [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' # Check whether we have something as basic as zephyr.elf file with tarfile.open(package_path, "r") as tar: assert any([path.endswith('zephyr.elf') for path in tar.getnames()]) # Delete everything but for the package for clean_up in os.listdir(os.path.join(out_path)): if not clean_up.endswith(package_name): clean_up_path = os.path.join(out_path, clean_up) if os.path.isfile(clean_up_path): os.remove(clean_up_path) else: shutil.rmtree(os.path.join(out_path, clean_up)) # Unpack the package with tarfile.open(package_path, "r") as tar: tar.extractall(path=out_path) # Why does package.py put files inside the out_path folder? # It forces us to move files up one directory after extraction. file_names = os.listdir(os.path.join(out_path, os.path.basename(out_path))) for file_name in file_names: shutil.move(os.path.join(out_path, os.path.basename(out_path), file_name), out_path) args = ['-i', '--outdir', out_path, '-T', path] + \ ['--test-only'] + \ [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'