From d860da47c048cda2ea7848ae4e49a540a661b752 Mon Sep 17 00:00:00 2001 From: Jakub Kocka Date: Mon, 26 Jan 2026 09:43:43 +0100 Subject: [PATCH] ci(tools): Avoiding full rebuilds where not needed to improve performance --- .gitlab/ci/test-win.yml | 7 ++- tools/test_build_system/test_partition.py | 25 +++++--- tools/test_build_system/test_spaces.py | 2 +- tools/test_idf_py/test_idf_py.py | 76 ++++++++++++----------- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/.gitlab/ci/test-win.yml b/.gitlab/ci/test-win.yml index bf2a116e91..a3a2c3950f 100644 --- a/.gitlab/ci/test-win.yml +++ b/.gitlab/ci/test-win.yml @@ -87,13 +87,13 @@ test_tools_win: - python "${SUBMODULE_FETCH_TOOL}" -s "all" - cd ${IDF_PATH}\tools\test_build_system - idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME} - - pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} + - pytest --parallel-count ${CI_NODE_TOTAL} --parallel-index ${CI_NODE_INDEX} --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} --durations=10 pytest_build_system_win: extends: - .test_build_system_template_win - .rules:labels:windows_pytest_build_system - parallel: 6 + parallel: 10 needs: - job: manual_gate optional: true @@ -111,6 +111,7 @@ pytest_build_system_win_minimal_cmake: extends: - .test_build_system_template_win - .rules:labels:windows_pytest_build_system + parallel: 2 needs: - job: manual_gate optional: true @@ -139,7 +140,7 @@ pytest_build_system_win_minimal_cmake: - python "${SUBMODULE_FETCH_TOOL}" -s "all" - cd ${IDF_PATH}\tools\test_build_system - idf-ci gitlab download-known-failure-cases-file ${KNOWN_FAILURE_CASES_FILE_NAME} - - pytest -k cmake --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} + - pytest -k cmake --junitxml=${CI_PROJECT_DIR}\XUNIT_RESULT.xml --ignore-result-files ${KNOWN_FAILURE_CASES_FILE_NAME} --durations=10 pytest_buildv2_system_win: extends: diff --git a/tools/test_build_system/test_partition.py b/tools/test_build_system/test_partition.py index b8947cba92..83885d2f0e 100644 --- a/tools/test_build_system/test_partition.py +++ b/tools/test_build_system/test_partition.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2022-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import logging @@ -8,7 +8,10 @@ import shutil from pathlib import Path import pytest -from test_build_system_helpers import EnvDict, IdfPyFunc, append_to_file, replace_in_file +from test_build_system_helpers import EnvDict +from test_build_system_helpers import IdfPyFunc +from test_build_system_helpers import append_to_file +from test_build_system_helpers import replace_in_file @pytest.mark.usefixtures('test_app_copy') @@ -30,15 +33,21 @@ def test_partition_nearly_full_warning(idf_py: IdfPyFunc, test_app_copy: Path, d logging.info('Warning is given if smallest partition is nearly full') ret = idf_py('build') # Build a first time to get the binary size and to check that no warning is issued. - assert 'partition is nearly full' not in ret.stdout, 'Warning for nearly full smallest partition was given when the condition is not fulfilled' + assert 'partition is nearly full' not in ret.stdout, ( + 'Warning for nearly full smallest partition was given when the condition is not fulfilled' + ) # Get the size of the binary, in KB. Convert it to next multiple of 4. # The goal is to create an app partition which is slightly bigger than the binary itself updated_file_size = int((os.stat(test_app_copy / 'build' / 'build_test_app.bin').st_size + 4095) / 4096) * 4 idf_path = Path(default_idf_env['IDF_PATH']) - shutil.copy2(idf_path / 'components' / 'partition_table' / 'partitions_singleapp.csv', test_app_copy / 'partitions.csv') - replace_in_file(test_app_copy / 'partitions.csv', - 'factory, app, factory, , 1M', - f'factory, app, factory, , {updated_file_size}K') - (test_app_copy / 'sdkconfig').write_text('\n'.join(['CONFIG_PARTITION_TABLE_CUSTOM=y', 'CONFIG_FREERTOS_SMP=n'])) + shutil.copy2( + idf_path / 'components' / 'partition_table' / 'partitions_singleapp.csv', test_app_copy / 'partitions.csv' + ) + replace_in_file( + test_app_copy / 'partitions.csv', + 'factory, app, factory, , 1M', + f'factory, app, factory, , {updated_file_size}K', + ) + append_to_file(test_app_copy / 'sdkconfig', '\n'.join(['CONFIG_PARTITION_TABLE_CUSTOM=y', 'CONFIG_FREERTOS_SMP=n'])) ret = idf_py('build', check=False) assert 'partition is nearly full' in ret.stdout diff --git a/tools/test_build_system/test_spaces.py b/tools/test_build_system/test_spaces.py index 438bb4853b..8ef2895031 100644 --- a/tools/test_build_system/test_spaces.py +++ b/tools/test_build_system/test_spaces.py @@ -82,7 +82,7 @@ def test_spaces_bundle3(idf_copy: Path) -> None: def test_spaces_bundle4(dummy_: str, idf_py: IdfPyFunc, test_app_copy: Path) -> None: logging.info(f'Running test spaces bundle 4 in {test_app_copy}') (test_app_copy / 'sdkconfig').write_text('CONFIG_APP_REPRODUCIBLE_BUILD=y') - idf_py('build') + idf_py('reconfigure') (test_app_copy / 'sdkconfig').unlink() idf_py('set-target', 'esp32s2') diff --git a/tools/test_idf_py/test_idf_py.py b/tools/test_idf_py/test_idf_py.py index 9f2ea22053..b8c145e7e1 100755 --- a/tools/test_idf_py/test_idf_py.py +++ b/tools/test_idf_py/test_idf_py.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -# SPDX-FileCopyrightText: 2019-2025 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2019-2026 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Apache-2.0 import json import os @@ -50,48 +50,54 @@ class TestWithoutExtensions(TestCase): class TestExtensions(TestWithoutExtensions): - def test_extension_loading(self): + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create symlink once for all tests in this class + # Handle race conditions with parallel test execution (pytest-xdist) try: os.symlink(extension_path, link_path) - os.environ['IDF_EXTRA_ACTIONS_PATH'] = os.path.join(current_dir, 'extra_path') - output = subprocess.check_output([sys.executable, idf_py_path, '--help'], env=os.environ).decode( - 'utf-8', 'ignore' - ) + except FileExistsError: + # Another worker already created it - that's fine + pass + os.environ['IDF_EXTRA_ACTIONS_PATH'] = os.path.join(current_dir, 'extra_path') - self.assertIn('--test-extension-option', output) - self.assertIn('test_subcommand', output) - self.assertIn('--some-extension-option', output) - self.assertIn('extra_subcommand', output) - finally: + @classmethod + def tearDownClass(cls): + # Clean up symlink after all tests complete + # Use try/except to handle race conditions with parallel execution + try: os.remove(link_path) + except FileNotFoundError: + # Another worker already removed it - that's fine + pass + super().tearDownClass() + + def test_extension_loading(self): + output = subprocess.check_output([sys.executable, idf_py_path, '--help'], env=os.environ).decode( + 'utf-8', 'ignore' + ) + self.assertIn('--test-extension-option', output) + self.assertIn('test_subcommand', output) + self.assertIn('--some-extension-option', output) + self.assertIn('extra_subcommand', output) def test_extension_execution(self): - try: - os.symlink(extension_path, link_path) - os.environ['IDF_EXTRA_ACTIONS_PATH'] = ';'.join([os.path.join(current_dir, 'extra_path')]) - output = subprocess.check_output( - [sys.executable, idf_py_path, '--some-extension-option=awesome', 'test_subcommand', 'extra_subcommand'], - env=os.environ, - ).decode('utf-8', 'ignore') - self.assertIn('!!! From some global callback: awesome', output) - self.assertIn('!!! From some subcommand', output) - self.assertIn('!!! From test global callback: test', output) - self.assertIn('!!! From some subcommand', output) - finally: - os.remove(link_path) + output = subprocess.check_output( + [sys.executable, idf_py_path, '--some-extension-option=awesome', 'test_subcommand', 'extra_subcommand'], + env=os.environ, + ).decode('utf-8', 'ignore') + self.assertIn('!!! From some global callback: awesome', output) + self.assertIn('!!! From some subcommand', output) + self.assertIn('!!! From test global callback: test', output) + self.assertIn('!!! From some subcommand', output) def test_hidden_commands(self): - try: - os.symlink(extension_path, link_path) - os.environ['IDF_EXTRA_ACTIONS_PATH'] = ';'.join([os.path.join(current_dir, 'extra_path')]) - output = subprocess.check_output([sys.executable, idf_py_path, '--help'], env=os.environ).decode( - 'utf-8', 'ignore' - ) - self.assertIn('test_subcommand', output) - self.assertNotIn('hidden_one', output) - - finally: - os.remove(link_path) + output = subprocess.check_output([sys.executable, idf_py_path, '--help'], env=os.environ).decode( + 'utf-8', 'ignore' + ) + self.assertIn('test_subcommand', output) + self.assertNotIn('hidden_one', output) class TestDependencyManagement(TestWithoutExtensions):