test_common.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. import json
  4. import logging
  5. import os
  6. import shutil
  7. import stat
  8. import subprocess
  9. import sys
  10. import textwrap
  11. from pathlib import Path
  12. from typing import List
  13. import pytest
  14. from test_build_system_helpers import (EnvDict, IdfPyFunc, append_to_file, file_contains, find_python, get_snapshot,
  15. replace_in_file, run_idf_py)
  16. def get_subdirs_absolute_paths(path: Path) -> List[str]:
  17. """
  18. Get a list of files with absolute path in a given `path` folder
  19. """
  20. return [
  21. '{}/{}'.format(dir_path, file_name)
  22. for dir_path, _, file_names in os.walk(path)
  23. for file_name in file_names
  24. ]
  25. @pytest.mark.usefixtures('test_app_copy')
  26. @pytest.mark.test_app_copy('examples/get-started/blink')
  27. def test_compile_commands_json_updated_by_reconfigure(idf_py: IdfPyFunc) -> None:
  28. output = idf_py('reconfigure')
  29. assert 'Building ESP-IDF components for target esp32' in output.stdout
  30. snapshot_1 = get_snapshot(['build/compile_commands.json'])
  31. snapshot_2 = get_snapshot(['build/compile_commands.json'])
  32. snapshot_2.assert_same(snapshot_1)
  33. idf_py('reconfigure')
  34. snapshot_3 = get_snapshot(['build/compile_commands.json'])
  35. snapshot_3.assert_different(snapshot_2)
  36. @pytest.mark.usefixtures('test_app_copy')
  37. def test_of_test_app_copy(idf_py: IdfPyFunc) -> None:
  38. p = Path('main/idf_component.yml')
  39. p.write_text('syntax_error\n')
  40. try:
  41. with (pytest.raises(subprocess.CalledProcessError)) as exc_info:
  42. idf_py('reconfigure')
  43. assert 'ERROR: Unknown format of the manifest file:' in exc_info.value.stderr
  44. finally:
  45. p.unlink()
  46. @pytest.mark.usefixtures('test_app_copy')
  47. def test_hints_no_color_output_when_noninteractive(idf_py: IdfPyFunc) -> None:
  48. """Check that idf.py hints don't include color escape codes in non-interactive builds"""
  49. # make the build fail in such a way that idf.py shows a hint
  50. replace_in_file('main/build_test_app.c', '// placeholder_inside_main',
  51. 'esp_chip_info_t chip_info; esp_chip_info(&chip_info);')
  52. with (pytest.raises(subprocess.CalledProcessError)) as exc_info:
  53. idf_py('build')
  54. # Should not actually include a color escape sequence!
  55. # Change the assert to the correct value once the bug is fixed.
  56. assert '\x1b[0;33mHINT: esp_chip_info.h' in exc_info.value.stderr
  57. @pytest.mark.usefixtures('test_app_copy')
  58. def test_idf_copy(idf_copy: Path, idf_py: IdfPyFunc) -> None:
  59. # idf_copy is the temporary IDF copy.
  60. # For example, we can check if idf.py build can work without the .git directory:
  61. shutil.rmtree(idf_copy / '.git', ignore_errors=True)
  62. idf_py('build')
  63. def test_idf_build_with_env_var_sdkconfig_defaults(
  64. test_app_copy: Path,
  65. default_idf_env: EnvDict
  66. ) -> None:
  67. with open(test_app_copy / 'sdkconfig.test', 'w') as fw:
  68. fw.write('CONFIG_BT_ENABLED=y')
  69. default_idf_env['SDKCONFIG_DEFAULTS'] = 'sdkconfig.test'
  70. run_idf_py('build', env=default_idf_env)
  71. with open(test_app_copy / 'sdkconfig') as fr:
  72. assert 'CONFIG_BT_ENABLED=y' in fr.read()
  73. @pytest.mark.usefixtures('test_app_copy')
  74. @pytest.mark.test_app_copy('examples/system/efuse')
  75. def test_efuse_summary_cmake_functions(
  76. default_idf_env: EnvDict
  77. ) -> None:
  78. default_idf_env['IDF_CI_BUILD'] = '1'
  79. output = run_idf_py('efuse-summary', env=default_idf_env)
  80. assert 'FROM_CMAKE: MAC: 00:00:00:00:00:00' in output.stdout
  81. assert 'FROM_CMAKE: WR_DIS: 0' in output.stdout
  82. def test_custom_build_folder(test_app_copy: Path, idf_py: IdfPyFunc) -> None:
  83. idf_py('-BBuiLDdiR', 'build')
  84. assert (test_app_copy / 'BuiLDdiR').is_dir()
  85. def test_python_clean(idf_py: IdfPyFunc) -> None:
  86. logging.info('Cleaning Python bytecode')
  87. idf_path = Path(os.environ['IDF_PATH'])
  88. abs_paths = get_subdirs_absolute_paths(idf_path)
  89. abs_paths_suffix = [path for path in abs_paths if path.endswith(('.pyc', '.pyo'))]
  90. assert len(abs_paths_suffix) != 0
  91. idf_py('python-clean')
  92. abs_paths = get_subdirs_absolute_paths(idf_path)
  93. abs_paths_suffix = [path for path in abs_paths if path.endswith(('.pyc', '.pyo'))]
  94. assert len(abs_paths_suffix) == 0
  95. @pytest.mark.skipif(sys.platform == 'win32', reason='Windows does not support executing bash script')
  96. def test_python_interpreter_unix(test_app_copy: Path) -> None:
  97. logging.info("Make sure idf.py never runs '/usr/bin/env python' or similar")
  98. env_dict = dict(**os.environ)
  99. python = find_python(env_dict['PATH'])
  100. (test_app_copy / 'python').write_text(textwrap.dedent("""#!/bin/sh
  101. echo "idf.py has executed '/usr/bin/env python' or similar"
  102. exit 1
  103. """))
  104. st = os.stat(test_app_copy / 'python')
  105. # equivalent to 'chmod +x ./python'
  106. os.chmod((test_app_copy / 'python'), st.st_mode | stat.S_IEXEC)
  107. env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH']
  108. # python is loaded from env:$PATH, but since false interpreter is provided there, python needs to be specified as argument
  109. # if idf.py is reconfigured during it's execution, it would load a false interpreter
  110. run_idf_py('reconfigure', env=env_dict, python=python)
  111. @pytest.mark.skipif(sys.platform != 'win32', reason='Linux does not support executing .exe files')
  112. def test_python_interpreter_win(test_app_copy: Path) -> None:
  113. logging.info('Make sure idf.py never runs false python interpreter')
  114. env_dict = dict(**os.environ)
  115. python = find_python(env_dict['PATH'])
  116. # on windows python interpreter has compiled code '.exe' format, so this false file can be empty
  117. (test_app_copy / 'python.exe').write_text('')
  118. env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH']
  119. # python is loaded from env:$PATH, but since false interpreter is provided there, python needs to be specified as argument
  120. # if idf.py is reconfigured during it's execution, it would load a false interpreter
  121. run_idf_py('reconfigure', env=env_dict, python=python)
  122. @pytest.mark.usefixtures('test_app_copy')
  123. def test_invoke_confserver(idf_py: IdfPyFunc) -> None:
  124. logging.info('Confserver can be invoked by idf.py')
  125. idf_py('confserver', input_str='{"version": 1}')
  126. def test_ccache_used_to_build(test_app_copy: Path) -> None:
  127. logging.info('Check ccache is used to build')
  128. (test_app_copy / 'ccache').touch(mode=0o755)
  129. env_dict = dict(**os.environ)
  130. env_dict['PATH'] = str(test_app_copy) + os.pathsep + env_dict['PATH']
  131. # Disable using ccache automatically
  132. if 'IDF_CCACHE_ENABLE' in env_dict:
  133. env_dict.pop('IDF_CCACHE_ENABLE')
  134. ret = run_idf_py('--ccache', 'reconfigure', env=env_dict)
  135. assert 'ccache will be used' in ret.stdout
  136. run_idf_py('fullclean', env=env_dict)
  137. ret = run_idf_py('reconfigure', env=env_dict)
  138. assert 'ccache will be used' not in ret.stdout
  139. ret = run_idf_py('--no-ccache', 'reconfigure', env=env_dict)
  140. assert 'ccache will be used' not in ret.stdout
  141. def test_toolchain_prefix_in_description_file(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
  142. logging.info('Toolchain prefix is set in project description file')
  143. idf_py('reconfigure')
  144. data = json.load(open(test_app_copy / 'build' / 'project_description.json', 'r'))
  145. assert 'monitor_toolprefix' in data
  146. @pytest.mark.usefixtures('test_app_copy')
  147. def test_subcommands_with_options(idf_py: IdfPyFunc, default_idf_env: EnvDict) -> None:
  148. logging.info('Can set options to subcommands: print_filter for monitor')
  149. idf_path = Path(default_idf_env.get('IDF_PATH'))
  150. # try - finally block is here used to backup and restore idf_monitor.py
  151. # since we need to handle only one file, this souluton is much faster than using idf_copy fixture
  152. monitor_backup = (idf_path / 'tools' / 'idf_monitor.py').read_text()
  153. try:
  154. (idf_path / 'tools' / 'idf_monitor.py').write_text('import sys;print(sys.argv[1:])')
  155. idf_py('build')
  156. ret = idf_py('monitor', '--print-filter=*:I', '-p', 'tty.fake')
  157. assert "'--print_filter', '*:I'" in ret.stdout
  158. finally:
  159. (idf_path / 'tools' / 'idf_monitor.py').write_text(monitor_backup)
  160. def test_fallback_to_build_system_target(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
  161. logging.info('idf.py fallback to build system target')
  162. msg = 'Custom target is running'
  163. append_to_file(test_app_copy / 'CMakeLists.txt',
  164. 'add_custom_target(custom_target COMMAND ${{CMAKE_COMMAND}} -E echo "{}")'.format(msg))
  165. ret = idf_py('custom_target')
  166. assert msg in ret.stdout, 'Custom target did not produce expected output'
  167. def test_create_component_and_project_plus_build(idf_copy: Path) -> None:
  168. logging.info('Create project and component using idf.py and build it')
  169. run_idf_py('-C', 'projects', 'create-project', 'temp_test_project', workdir=idf_copy)
  170. run_idf_py('-C', 'components', 'create-component', 'temp_test_component', workdir=idf_copy)
  171. replace_in_file(idf_copy / 'projects' / 'temp_test_project' / 'main' / 'temp_test_project.c', '{\n\n}',
  172. '\n'.join(['{', '\tfunc();', '}']))
  173. replace_in_file(idf_copy / 'projects' / 'temp_test_project' / 'main' / 'temp_test_project.c', '#include <stdio.h>',
  174. '\n'.join(['#include <stdio.h>', '#include "temp_test_component.h"']))
  175. run_idf_py('build', workdir=(idf_copy / 'projects' / 'temp_test_project'))
  176. # In this test function, there are actually two logical tests in one test function.
  177. # It would be better to have every check in a separate
  178. # test case, but that would mean doing idf_copy each time, and copying takes most of the time
  179. def test_create_project(idf_py: IdfPyFunc, idf_copy: Path) -> None:
  180. logging.info('Check that command for creating new project will fail if the target folder is not empty.')
  181. (idf_copy / 'example_proj').mkdir()
  182. (idf_copy / 'example_proj' / 'tmp_1').touch()
  183. ret = idf_py('create-project', '--path', str(idf_copy / 'example_proj'), 'temp_test_project', check=False)
  184. assert ret.returncode == 3, 'Command create-project exit value is wrong.'
  185. # cleanup for the following test
  186. shutil.rmtree(idf_copy / 'example_proj')
  187. logging.info('Check that command for creating new project will fail if the target path is file.')
  188. (idf_copy / 'example_proj_file').touch()
  189. ret = idf_py('create-project', '--path', str(idf_copy / 'example_proj_file'), 'temp_test_project', check=False)
  190. assert ret.returncode == 4, 'Command create-project exit value is wrong.'
  191. @pytest.mark.skipif(sys.platform == 'darwin', reason='macos runner is a shell executor, it would break the file system')
  192. def test_create_project_with_idf_readonly(idf_copy: Path) -> None:
  193. def change_to_readonly(src: Path) -> None:
  194. for root, dirs, files in os.walk(src):
  195. for name in dirs:
  196. os.chmod(os.path.join(root, name), 0o555) # read & execute
  197. for name in files:
  198. path = os.path.join(root, name)
  199. if '/bin/' in path:
  200. continue # skip excutables
  201. os.chmod(os.path.join(root, name), 0o444) # readonly
  202. logging.info('Check that command for creating new project will success if the IDF itself is readonly.')
  203. change_to_readonly(idf_copy)
  204. run_idf_py('create-project', '--path', str(idf_copy / 'example_proj'), 'temp_test_project')
  205. @pytest.mark.usefixtures('test_app_copy')
  206. def test_docs_command(idf_py: IdfPyFunc) -> None:
  207. logging.info('Check docs command')
  208. idf_py('set-target', 'esp32')
  209. ret = idf_py('docs', '--no-browser')
  210. assert 'https://docs.espressif.com/projects/esp-idf/en' in ret.stdout
  211. ret = idf_py('docs', '--no-browser', '--language', 'en')
  212. assert 'https://docs.espressif.com/projects/esp-idf/en' in ret.stdout
  213. ret = idf_py('docs', '--no-browser', '--language', 'en', '--version', 'v4.2.1')
  214. assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1' in ret.stdout
  215. ret = idf_py('docs', '--no-browser', '--language', 'en', '--version', 'v4.2.1', '--target', 'esp32')
  216. assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1/esp32' in ret.stdout
  217. ret = idf_py('docs', '--no-browser', '--language', 'en', '--version', 'v4.2.1', '--target', 'esp32', '--starting-page', 'get-started')
  218. assert 'https://docs.espressif.com/projects/esp-idf/en/v4.2.1/esp32/get-started' in ret.stdout
  219. @pytest.mark.usefixtures('test_app_copy')
  220. def test_deprecation_warning(idf_py: IdfPyFunc) -> None:
  221. logging.info('Deprecation warning check')
  222. ret = idf_py('post_debug', check=False)
  223. # click warning
  224. assert 'Error: Command "post_debug" is deprecated since v4.4 and was removed in v5.0.' in ret.stderr
  225. ret = idf_py('efuse_common_table', check=False)
  226. # cmake warning
  227. assert 'Have you wanted to run "efuse-common-table" instead?' in ret.stdout
  228. def test_save_defconfig_check(idf_py: IdfPyFunc, test_app_copy: Path) -> None:
  229. logging.info('Save-defconfig checks')
  230. (test_app_copy / 'sdkconfig').write_text('\n'.join(['CONFIG_COMPILER_OPTIMIZATION_SIZE=y',
  231. 'CONFIG_ESPTOOLPY_FLASHFREQ_80M=y']))
  232. idf_py('save-defconfig')
  233. assert not file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_IDF_TARGET'), \
  234. 'CONFIG_IDF_TARGET should not be in sdkconfig.defaults'
  235. assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_COMPILER_OPTIMIZATION_SIZE=y'), \
  236. 'Missing CONFIG_COMPILER_OPTIMIZATION_SIZE=y in sdkconfig.defaults'
  237. assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_ESPTOOLPY_FLASHFREQ_80M=y'), \
  238. 'Missing CONFIG_ESPTOOLPY_FLASHFREQ_80M=y in sdkconfig.defaults'
  239. idf_py('fullclean')
  240. (test_app_copy / 'sdkconfig').unlink()
  241. (test_app_copy / 'sdkconfig.defaults').unlink()
  242. idf_py('set-target', 'esp32c3')
  243. (test_app_copy / 'sdkconfig').write_text('CONFIG_PARTITION_TABLE_OFFSET=0x8001')
  244. idf_py('save-defconfig')
  245. assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_IDF_TARGET="esp32c3"'), \
  246. 'Missing CONFIG_IDF_TARGET="esp32c3" in sdkconfig.defaults'
  247. assert file_contains(test_app_copy / 'sdkconfig.defaults', 'CONFIG_PARTITION_TABLE_OFFSET=0x8001'), \
  248. 'Missing CONFIG_PARTITION_TABLE_OFFSET=0x8001 in sdkconfig.defaults'