#!/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 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', 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', 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\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', 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'