test_idf_tools_python_env.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
  2. # SPDX-License-Identifier: Apache-2.0
  3. # NOTE: unittest is by default sorting tests based on their names,
  4. # so the order if which the tests are started may be different from
  5. # the order in which they are defined. Please make sure all tests
  6. # are entirely self contained and don't have affect on other tests,
  7. # for example by changing some global state, like system environment.
  8. # If test needs to change global state, it should return it to the
  9. # original state after it's finished. For more information please see
  10. # https://docs.python.org/3/library/unittest.html#organizing-test-code
  11. import inspect
  12. import os
  13. import shutil
  14. import subprocess
  15. import sys
  16. import tempfile
  17. import unittest
  18. from typing import List
  19. try:
  20. import idf_tools
  21. except ImportError:
  22. sys.path.append('..')
  23. import idf_tools
  24. IDF_PATH = os.environ.get('IDF_PATH', '../..')
  25. TOOLS_DIR = os.environ.get('IDF_TOOLS_PATH') or os.path.expanduser(idf_tools.IDF_TOOLS_PATH_DEFAULT)
  26. PYTHON_DIR = os.path.join(TOOLS_DIR, 'python_env')
  27. REQ_SATISFIED = 'Python requirements are satisfied'
  28. REQ_CORE = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.core.txt'))
  29. REQ_GDBGUI = '- {}'.format(os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.gdbgui.txt'))
  30. CONSTR = 'Constraint file: {}/espidf.constraints'.format(TOOLS_DIR)
  31. # Set default global paths for idf_tools. If some test needs to
  32. # use functions from idf_tools with custom paths, it should
  33. # set it in setUp() and change them back to defaults in tearDown().
  34. idf_tools.global_idf_path = IDF_PATH
  35. idf_tools.global_idf_tools_path = TOOLS_DIR
  36. class BasePythonInstall(unittest.TestCase):
  37. def run_tool(self, cmd): # type: (List[str]) -> str
  38. ret = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300)
  39. decoded_output = ret.stdout.decode('utf-8', 'ignore')
  40. with open(os.path.join(IDF_PATH, 'tools', 'test_idf_tools', 'test_python_env_logs.txt'), 'a+') as w:
  41. # stack() returns list of callers frame records. [1] represent caller of this function
  42. w.write('============================= ' + inspect.stack()[1].function + ' =============================\n')
  43. w.write(decoded_output)
  44. return decoded_output
  45. def run_idf_tools(self, args): # type: (List[str]) -> str
  46. cmd = [sys.executable, '../idf_tools.py'] + args
  47. return self.run_tool(cmd)
  48. def run_in_venv(self, args): # type: (List[str]) -> str
  49. _, _, python_venv, _ = idf_tools.get_python_env_path()
  50. cmd = [python_venv] + args
  51. return self.run_tool(cmd)
  52. def dump_foopackage(self): # type: () -> str
  53. # Wheel for foopackage-0.99-py3-none-any.whl
  54. # This is dummy package for testing purposes created with
  55. # python -m build --wheel for the following package
  56. '''
  57. ├── foopackage
  58. │   └── __init__.py
  59. └── setup.py
  60. setup.py
  61. from setuptools import setup
  62. setup(
  63. name="foopackage",
  64. version="0.99",
  65. )
  66. __init__.py
  67. if __name__ == '__main__':
  68. return
  69. '''
  70. whl = (b'PK\x03\x04\x14\x00\x00\x00\x08\x00\x07fqVz|E\t&\x00\x00\x00&\x00\x00\x00\x16\x00\x00\x00'
  71. b'foopackage/__init__.py\xcbLS\x88\x8f\xcfK\xccM\x8d\x8fW\xb0\xb5UP\x8f\x8f\xcfM\xcc\xcc\x8b\x8fW'
  72. b'\xb7\xe2R\x00\x82\xa2\xd4\x92\xd2\xa2<.\x00PK\x03\x04\x14\x00\x00\x00\x08\x00%fqV\x8d\x90\x81\x05'
  73. b'1\x00\x00\x006\x00\x00\x00"\x00\x00\x00foopackage-0.99.dist-info/METADATA\xf3M-ILI,I\xd4\rK-*\xce'
  74. b'\xcc\xcf\xb3R0\xd23\xe4\xf2K\xccM\xb5RH\xcb\xcf/HL\xceNLO\xe5\x82\xcb\x1a\xe8YZrq\x01\x00PK\x03\x04'
  75. b'\x14\x00\x00\x00\x08\x00%fqVI\xa2!\xcb\\\x00\x00\x00\\\x00\x00\x00\x1f\x00\x00\x00foopackage-0.99'
  76. b'.dist-info/WHEEL\x0b\xcfHM\xcd\xd1\rK-*\xce\xcc\xcf\xb3R0\xd43\xe0rO\xcdK-J,\xc9/\xb2RHJ\xc9,.\x89/'
  77. b'\x07\xa9Q\xd00\xd031\xd03\xd0\xe4\n\xca\xcf/\xd1\xf5,\xd6\r(-J\xcd\xc9L\xb2R()*M\xe5\nIL\xb7R(\xa84'
  78. b'\xd6\xcd\xcb\xcfK\xd5M\xcc\xab\xe4\xe2\x02\x00PK\x03\x04\x14\x00\x00\x00\x08\x00%fqVI*\x9e\xa7\r\x00'
  79. b'\x00\x00\x0b\x00\x00\x00\'\x00\x00\x00foopackage-0.99.dist-info/top_level.txtK\xcb\xcf/HL\xceNLO\xe5'
  80. b'\x02\x00PK\x03\x04\x14\x00\x00\x00\x08\x00%fqV&\xdc\x9b\x88\xfd\x00\x00\x00}\x01\x00\x00 \x00\x00\x00'
  81. b'foopackage-0.99.dist-info/RECORD}\xcc;\x92\x820\x00\x00\xd0\xde\xb3\x04\xe4#\xbfb\x8b\xac\xb0\x0b,'
  82. b'\xa8\x83\x02#M&\x08\x81\x80\x02c\x02\x82\xa7\xb7rK\xdf\x01\x1e\xe9\xfb\x01_Z\\\x95k\x84hG9B\xe2\xb0'
  83. b'\x00VcE\xd3\xbf\xf4\xe6\xe1\t6a2\xc3\x16N\x06]1Bm\xb7\x17\xc2Z\xef\xaa\xed\xf6\x9c\xdaQ \xd0\xf6\xc6'
  84. b':\xec\x00\xd5\\\x91\xffL\x90D\xcb\x12\x0b\xca\xb8@;\xd2\xafC\xe7\x04mx\x82\xef\xb8\xf2\xc6"\xd9\xdd'
  85. b'\r\x18\xe4\xcd\xef=\xf7\n7\x9eg4?\xa7\x04V*gXI\xff\xcanD\xc1\xf1\xc0\x80\xb6\xf9\x10\xa7\xae\xe3\x04'
  86. b'\xefuh/<;?\xe3\xe3\x06\x9e\x93N/|\xc1Puc\xefgt\xfaQJ3\x82V\x8e\xb2\xef\x86\x12\xd9\x04\x96\xf2a\xe5'
  87. b'\xfd\x80\xae\xe5T^E>\xf3\xf7\x1eW\x122\xe4\x91\xfbi\x1f\xd6\xeem\x99\xd4\xec\x11Ju\x9d\'R\xc83R\x19>'
  88. b'jbO:\xb8\x8b\td\xf9\xc3\x1e9\xdb}d\x03\xb0z\x01PK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00\x07fqVz|E\t'
  89. b'&\x00\x00\x00&\x00\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00'
  90. b'foopackage/__init__.pyPK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00%fqV\x8d\x90\x81\x051\x00\x00\x006\x00'
  91. b'\x00\x00"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81Z\x00\x00\x00foopackage-0.99.dist-info'
  92. b'/METADATAPK\x01\x02\x14\x03\x14\x00\x00\x00\x08\x00%fqVI\xa2!\xcb\\\x00\x00\x00\\\x00\x00\x00\x1f\x00'
  93. b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\xcb\x00\x00\x00foopackage-0.99.dist-info/WHEELPK\x01'
  94. b'\x02\x14\x03\x14\x00\x00\x00\x08\x00%fqVI*\x9e\xa7\r\x00\x00\x00\x0b\x00\x00\x00\'\x00\x00\x00\x00\x00'
  95. b'\x00\x00\x00\x00\x00\x00\xa4\x81d\x01\x00\x00foopackage-0.99.dist-info/top_level.txtPK\x01\x02\x14\x03'
  96. b'\x14\x00\x00\x00\x08\x00%fqV&\xdc\x9b\x88\xfd\x00\x00\x00}\x01\x00\x00 \x00\x00\x00\x00\x00\x00\x00'
  97. b'\x00\x00\x00\x00\xb4\x81\xb6\x01\x00\x00foopackage-0.99.dist-info/RECORDPK\x05\x06\x00\x00\x00\x00\x05'
  98. b'\x00\x05\x00\x84\x01\x00\x00\xf1\x02\x00\x00\x00\x00')
  99. tmpdir = tempfile.mkdtemp()
  100. foopackage_fn = os.path.join(tmpdir, 'foopackage-0.99-py3-none-any.whl')
  101. with open(foopackage_fn, 'wb') as fd:
  102. fd.write(whl)
  103. self.addCleanup(shutil.rmtree, tmpdir)
  104. return foopackage_fn
  105. class TestPythonInstall(BasePythonInstall):
  106. def setUp(self): # type: () -> None
  107. if os.path.isdir(PYTHON_DIR):
  108. shutil.rmtree(PYTHON_DIR)
  109. if os.path.isfile(os.path.join(TOOLS_DIR, 'idf-env.json')):
  110. os.remove(os.path.join(TOOLS_DIR, 'idf-env.json'))
  111. def test_default_arguments(self): # type: () -> None
  112. output = self.run_idf_tools(['check-python-dependencies'])
  113. self.assertNotIn(REQ_SATISFIED, output)
  114. self.assertIn('bin/python doesn\'t exist', output)
  115. output = self.run_idf_tools(['install-python-env'])
  116. self.assertIn(CONSTR, output)
  117. self.assertIn(REQ_CORE, output)
  118. self.assertNotIn(REQ_GDBGUI, output)
  119. output = self.run_idf_tools(['check-python-dependencies'])
  120. self.assertIn(REQ_SATISFIED, output)
  121. def test_opt_argument(self): # type: () -> None
  122. output = self.run_idf_tools(['install-python-env', '--features', 'gdbgui'])
  123. self.assertIn(CONSTR, output)
  124. self.assertIn(REQ_CORE, output)
  125. self.assertIn(REQ_GDBGUI, output)
  126. output = self.run_idf_tools(['install-python-env'])
  127. # The gdbgui should be installed as well because the feature is is stored in the JSON file
  128. self.assertIn(CONSTR, output)
  129. self.assertIn(REQ_CORE, output)
  130. self.assertIn(REQ_GDBGUI, output)
  131. # Argument that begins with '-' can't stand alone to be parsed as value
  132. output = self.run_idf_tools(['install-python-env', '--features=-gdbgui'])
  133. # After removing the gdbgui should not be present
  134. self.assertIn(CONSTR, output)
  135. self.assertIn(REQ_CORE, output)
  136. self.assertNotIn(REQ_GDBGUI, output)
  137. def test_no_constraints(self): # type: () -> None
  138. output = self.run_idf_tools(['install-python-env', '--no-constraints'])
  139. self.assertNotIn(CONSTR, output)
  140. self.assertIn(REQ_CORE, output)
  141. class TestCustomPythonPathInstall(BasePythonInstall):
  142. def setUp(self): # type: () -> None
  143. self.CUSTOM_PYTHON_DIR = tempfile.mkdtemp()
  144. self.environ_old = os.environ.copy()
  145. os.environ['IDF_PYTHON_ENV_PATH'] = self.CUSTOM_PYTHON_DIR
  146. def tearDown(self): # type: () -> None
  147. os.environ.clear()
  148. os.environ.update(self.environ_old)
  149. shutil.rmtree(self.CUSTOM_PYTHON_DIR)
  150. def test_default_arguments(self): # type: () -> None
  151. output = self.run_idf_tools(['check-python-dependencies'])
  152. self.assertIn(f"{self.CUSTOM_PYTHON_DIR}/bin/python doesn't exist", output)
  153. self.assertNotIn(PYTHON_DIR, output)
  154. output = self.run_idf_tools(['install-python-env'])
  155. self.assertIn(self.CUSTOM_PYTHON_DIR, output)
  156. self.assertNotIn(PYTHON_DIR, output)
  157. output = self.run_idf_tools(['check-python-dependencies'])
  158. self.assertIn(self.CUSTOM_PYTHON_DIR, output)
  159. class TestCheckPythonDependencies(BasePythonInstall):
  160. def setUp(self): # type: () -> None
  161. if os.path.isdir(PYTHON_DIR):
  162. shutil.rmtree(PYTHON_DIR)
  163. def tearDown(self): # type: () -> None
  164. if os.path.isdir(PYTHON_DIR):
  165. shutil.rmtree(PYTHON_DIR)
  166. def test_check_python_dependencies(self): # type: () -> None
  167. # Prepare artificial constraints file containing packages from
  168. # requirements.core.txt, which are also reported in pip-freeze output
  169. # for virtual env. The constraints file requires package versions higher
  170. # than currently installed in venv, so check_python_dependencies
  171. # should fail for all of them.
  172. self.run_idf_tools(['install-python-env'])
  173. freeze_output = self.run_in_venv(['-m', 'pip', 'freeze', '--all'])
  174. req_fn = os.path.join(IDF_PATH, 'tools', 'requirements', 'requirements.core.txt')
  175. with open(req_fn) as fd:
  176. req_list = [i for i in fd.read().splitlines() if i and i[0] != '#']
  177. # Create constrains list for packages in requirements.core.txt which
  178. # are also present in the freeze list.
  179. con_list = [r.replace('==', '>') for r in freeze_output.splitlines() if r.split('==')[0] in req_list]
  180. con_fn = idf_tools.get_constraints(idf_tools.get_idf_version(), online=False)
  181. # delete modified constraints file after this test is finished
  182. self.addCleanup(os.remove, con_fn)
  183. # Write the created constraints list into existing constraints file.
  184. # It will not be overwritten by subsequent idf_tools.py run, because
  185. # there is timestamp check.
  186. with open(con_fn, 'w') as fd:
  187. fd.write(os.linesep.join(con_list))
  188. # Test that check_python_dependencies reports that requirements are not satisfied for
  189. # all packages in the artificially created constrains file.
  190. output = self.run_idf_tools(['check-python-dependencies'])
  191. for con in [c.split('>')[0] for c in con_list]:
  192. self.assertIn(con, output)
  193. def test_check_required_packages_only(self): # type: () -> None
  194. # Test for espressif/esp-idf/-/merge_requests/17917
  195. # Install python env with core requirements, plus foopackage.
  196. # Add foopackage to constraints file requiring higher version
  197. # than currently installed. Since foopackage is not a direct
  198. # requirement, the dependency check should ignore it and should
  199. # not fail.
  200. self.run_idf_tools(['install-python-env'])
  201. foo_pkg = self.dump_foopackage()
  202. self.run_in_venv(['-m', 'pip', 'install', foo_pkg])
  203. con_fn = idf_tools.get_constraints(idf_tools.get_idf_version(), online=False)
  204. # delete modified constraints file after this test is finished
  205. self.addCleanup(os.remove, con_fn)
  206. # append foopackage constraint to the existing constraints file
  207. with open(con_fn, 'a') as fd:
  208. fd.write('foopackage>0.99')
  209. # check-python-dependencies should not complain about dummy_package
  210. output = self.run_idf_tools(['check-python-dependencies'])
  211. self.assertIn(REQ_SATISFIED, output)
  212. if __name__ == '__main__':
  213. unittest.main()