test_build_system_spaces.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env python
  2. # SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
  3. # SPDX-License-Identifier: Apache-2.0
  4. import os
  5. import shutil
  6. import subprocess
  7. import sys
  8. import typing
  9. import unittest
  10. try:
  11. IDF_PATH = os.environ['IDF_PATH'] # type: str
  12. except KeyError:
  13. print('IDF_PATH must be set before running this test', file=sys.stderr)
  14. exit(1)
  15. # Use the current directory for the builds
  16. TEST_PATH = os.getcwd()
  17. ##################
  18. # Helper functions
  19. ##################
  20. def find_python(path_var: str) -> str:
  21. """
  22. Find python interpreter in the paths specified in the given PATH variable.
  23. Returns the full path to the interpreter.
  24. """
  25. res = shutil.which('python', path=path_var)
  26. if res is None:
  27. raise ValueError('python not found')
  28. return res
  29. def get_idf_build_env(idf_path: typing.Optional[str]) -> typing.Dict[str, str]:
  30. """
  31. Get environment variables (as set by export.sh) for the specific IDF copy
  32. :param idf_path: if set, path of the IDF copy to use; otherwise, IDF_PATH from environment is used
  33. :return: dictionary of environment variables and their values
  34. """
  35. if idf_path is None:
  36. idf_path = IDF_PATH
  37. cmd = [
  38. sys.executable,
  39. os.path.join(idf_path, 'tools', 'idf_tools.py'),
  40. 'export',
  41. '--format=key-value'
  42. ]
  43. keys_values = subprocess.check_output(cmd).decode()
  44. env_vars = {key: os.path.expandvars(value) for key, value in
  45. [line.split('=') for line in keys_values.splitlines()]}
  46. # not set by idf_tools.py, normally set by export.sh
  47. env_vars['IDF_PATH'] = idf_path
  48. return env_vars
  49. def idf_py(*args: str, env_vars: typing.Optional[typing.Dict[str, str]] = None,
  50. idf_path: typing.Optional[str] = None,
  51. workdir: typing.Optional[str] = None) -> None:
  52. """
  53. Run idf.py command with given arguments, raise an exception on failure
  54. :param args: arguments to pass to idf.py
  55. :param env_vars: environment variables to run the build with; if not set, the default environment is used
  56. :param idf_path: path to the IDF copy to use; if not set, IDF_PATH from the environment is used
  57. :param workdir: directory where to run the build; if not set, the current directory is used
  58. """
  59. env = dict(**os.environ)
  60. if env_vars is not None:
  61. env.update(env_vars)
  62. if not workdir:
  63. workdir = os.getcwd()
  64. # order: function argument -> value in env dictionary -> system environment
  65. if idf_path is not None:
  66. inferred_idf_path = idf_path
  67. else:
  68. inferred_idf_path = str(env.get('IDF_PATH', IDF_PATH))
  69. python = find_python(env['PATH'])
  70. cmd = [
  71. python,
  72. os.path.join(inferred_idf_path, 'tools', 'idf.py')
  73. ]
  74. cmd += args # type: ignore
  75. subprocess.check_call(cmd, env=env, cwd=workdir)
  76. ############
  77. # Test cases
  78. ############
  79. class PathsWithSpaces(unittest.TestCase):
  80. IDF_PATH_WITH_SPACES = ''
  81. DO_CLEANUP = True
  82. ENV: typing.Dict[str, str] = dict()
  83. @classmethod
  84. def setUpClass(cls) -> None:
  85. if ' ' in IDF_PATH:
  86. print('IDF_PATH already contains spaces, not making a copy')
  87. cls.IDF_PATH_WITH_SPACES = IDF_PATH
  88. cls.DO_CLEANUP = False
  89. else:
  90. # Make a copy of ESP-IDF directory, with a space in its name
  91. cls.IDF_PATH_WITH_SPACES = os.path.join(TEST_PATH, 'esp idf')
  92. dest = cls.IDF_PATH_WITH_SPACES
  93. print('Copying esp-idf from {} to {}'.format(IDF_PATH, dest))
  94. shutil.copytree(IDF_PATH, dest,
  95. # if the CWD is inside the original esp-idf directory, make sure not to go into recursion.
  96. ignore=shutil.ignore_patterns(os.path.basename(dest)))
  97. cls.ENV = get_idf_build_env(cls.IDF_PATH_WITH_SPACES)
  98. @classmethod
  99. def tearDownClass(cls) -> None:
  100. if cls.DO_CLEANUP and os.path.exists(cls.IDF_PATH_WITH_SPACES):
  101. shutil.rmtree(cls.IDF_PATH_WITH_SPACES, ignore_errors=True)
  102. def test_install_export(self) -> None:
  103. env = dict(**os.environ)
  104. del env['IDF_PATH']
  105. if os.name == 'nt':
  106. install_cmd = 'install.bat esp32'
  107. export_cmd = 'export.bat'
  108. else:
  109. install_cmd = './install.sh esp32'
  110. export_cmd = '. ./export.sh'
  111. subprocess.check_call(install_cmd, env=env, shell=True, cwd=self.IDF_PATH_WITH_SPACES)
  112. if os.name == 'nt':
  113. subprocess.check_call(export_cmd, env=env, shell=True, cwd=self.IDF_PATH_WITH_SPACES)
  114. else:
  115. # The default shell used by subprocess.Popen on POSIX platforms is '/bin/sh',
  116. # which in esp-env Docker image is 'dash'. The export script doesn't support
  117. # IDF_PATH detection when used in dash, so we have to override the shell here.
  118. subprocess.check_call(export_cmd, env=env, shell=True, cwd=self.IDF_PATH_WITH_SPACES, executable='/bin/bash')
  119. def _copy_app_to(self, app_path: str, dest_dir_name: str) -> str:
  120. """
  121. Copy given app to a different directory, setting up cleanup hook.
  122. Destination directory is first deleted if it already exists.
  123. Returns the full path of the destination directory.
  124. app_path: path relative to esp-idf directory
  125. dest_dir_name: name of the destination directory, relative to the directory where the test is running
  126. """
  127. dest_dir = os.path.join(TEST_PATH, dest_dir_name)
  128. if os.path.exists(dest_dir) and os.path.isdir(dest_dir):
  129. shutil.rmtree(dest_dir, ignore_errors=True)
  130. src_dir = os.path.join(self.IDF_PATH_WITH_SPACES, app_path)
  131. shutil.copytree(os.path.join(src_dir), dest_dir)
  132. build_dir = os.path.join(dest_dir, 'build')
  133. if os.path.exists(build_dir):
  134. shutil.rmtree(build_dir, ignore_errors=True)
  135. self.addCleanup(shutil.rmtree, dest_dir, ignore_errors=True)
  136. return dest_dir
  137. # The tests below build different ESP-IDF apps (examples or test apps) to cover different parts
  138. # of the build system and related scripts.
  139. # In each test, IDF_PATH and app path both contain spaces.
  140. def test_build(self) -> None:
  141. build_path = self._copy_app_to(os.path.join('examples', 'get-started', 'hello_world'), 'test app')
  142. idf_py('build', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  143. def test_build_ulp_fsm(self) -> None:
  144. build_path = self._copy_app_to(os.path.join('examples', 'system', 'ulp_fsm', 'ulp'), 'test app')
  145. idf_py('build', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  146. def test_build_ulp_riscv(self) -> None:
  147. build_path = self._copy_app_to(os.path.join('examples', 'system', 'ulp_riscv', 'gpio'), 'test app')
  148. idf_py('-DIDF_TARGET=esp32s2', 'build', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES,
  149. workdir=build_path)
  150. def test_spiffsgen(self) -> None:
  151. build_path = self._copy_app_to(os.path.join('examples', 'storage', 'spiffsgen'), 'test app')
  152. idf_py('build', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  153. def test_flash_encryption(self) -> None:
  154. build_path = self._copy_app_to(os.path.join('examples', 'security', 'flash_encryption'), 'test app')
  155. idf_py('build', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  156. def test_secure_boot_v1(self) -> None:
  157. build_path = self._copy_app_to(os.path.join('tools', 'test_apps', 'security', 'secure_boot'), 'test app')
  158. idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.01', 'build', env_vars=self.ENV,
  159. idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  160. def test_secure_boot_v2(self) -> None:
  161. build_path = self._copy_app_to(os.path.join('tools', 'test_apps', 'security', 'secure_boot'), 'test app')
  162. idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.00', 'build', env_vars=self.ENV,
  163. idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  164. def test_app_signing(self) -> None:
  165. build_path = self._copy_app_to(os.path.join('tools', 'test_apps', 'security', 'secure_boot'), 'test app')
  166. idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.02', 'build', env_vars=self.ENV,
  167. idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  168. def test_secure_boot_release_mode(self) -> None:
  169. build_path = self._copy_app_to(os.path.join('tools', 'test_apps', 'security', 'secure_boot'), 'test app')
  170. idf_py('-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;sdkconfig.ci.04', '-DIDF_TARGET=esp32s2', 'build',
  171. env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  172. def test_x509_cert_bundle(self) -> None:
  173. build_path = self._copy_app_to(os.path.join('examples', 'protocols', 'https_x509_bundle'), 'test app')
  174. idf_py('build', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  175. def test_dfu(self) -> None:
  176. build_path = self._copy_app_to(os.path.join('examples', 'get-started', 'hello_world'), 'test app')
  177. idf_py('-DIDF_TARGET=esp32s2', 'dfu', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES,
  178. workdir=build_path)
  179. def test_uf2(self) -> None:
  180. build_path = self._copy_app_to(os.path.join('examples', 'get-started', 'hello_world'), 'test app')
  181. idf_py('uf2', env_vars=self.ENV, idf_path=self.IDF_PATH_WITH_SPACES, workdir=build_path)
  182. if __name__ == '__main__':
  183. unittest.main()