test_hints.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. #!/usr/bin/env python
  2. #
  3. # SPDX-FileCopyrightText: 2022-2023 Espressif Systems (Shanghai) CO LTD
  4. # SPDX-License-Identifier: Apache-2.0
  5. import os
  6. import sys
  7. import tempfile
  8. import unittest
  9. from pathlib import Path
  10. from subprocess import run
  11. from typing import List
  12. import yaml
  13. try:
  14. EXT_IDF_PATH = os.environ['IDF_PATH'] # type: str
  15. except KeyError:
  16. print(('This test needs to run within ESP-IDF environmnet. '
  17. 'Please run export script first.'), file=sys.stderr)
  18. exit(1)
  19. CWD = os.path.join(os.path.dirname(__file__))
  20. ERR_OUT_YML = os.path.join(CWD, 'error_output.yml')
  21. try:
  22. from idf_py_actions.tools import generate_hints
  23. except ImportError:
  24. sys.path.append(os.path.join(CWD, '..'))
  25. from idf_py_actions.tools import generate_hints
  26. class TestHintsMassages(unittest.TestCase):
  27. def setUp(self) -> None:
  28. self.tmpdir = tempfile.TemporaryDirectory()
  29. def test_output(self) -> None:
  30. with open(ERR_OUT_YML) as f:
  31. error_output = yaml.safe_load(f)
  32. error_filename = os.path.join(self.tmpdir.name, 'hint_input')
  33. for error, hint in error_output.items():
  34. with open(error_filename, 'w') as f:
  35. f.write(error)
  36. for generated_hint in generate_hints(f.name):
  37. self.assertEqual(generated_hint, hint)
  38. def tearDown(self) -> None:
  39. self.tmpdir.cleanup()
  40. def run_idf(args: List[str], cwd: Path) -> str:
  41. # Simple helper to run idf command and return it's stdout.
  42. cmd = [
  43. sys.executable,
  44. os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py')
  45. ]
  46. proc = run(cmd + args, capture_output=True, cwd=cwd, text=True)
  47. return str(proc.stdout + proc.stderr)
  48. class TestHintModuleComponentRequirements(unittest.TestCase):
  49. def setUp(self) -> None:
  50. # Set up a dummy project in tmp directory with main and component1 component.
  51. # The main component includes component1.h from component1, but the header dir is
  52. # not added in INCLUDE_DIRS and main doesn't have REQUIRES for component1.
  53. self.tmpdir = tempfile.TemporaryDirectory()
  54. self.tmpdirpath = Path(self.tmpdir.name)
  55. self.projectdir = self.tmpdirpath / 'project'
  56. self.projectdir.mkdir(parents=True)
  57. (self.projectdir / 'CMakeLists.txt').write_text((
  58. 'cmake_minimum_required(VERSION 3.16)\n'
  59. 'include($ENV{IDF_PATH}/tools/cmake/project.cmake)\n'
  60. 'project(foo)'))
  61. maindir = self.projectdir / 'main'
  62. maindir.mkdir()
  63. (maindir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "foo.c" REQUIRES esp_timer)')
  64. (maindir / 'foo.h').write_text('#include "component1.h"')
  65. (maindir / 'foo.c').write_text('#include "foo.h"\nvoid app_main(){}')
  66. component1dir = self.projectdir / 'components' / 'component1'
  67. component1dir.mkdir(parents=True)
  68. (component1dir / 'CMakeLists.txt').write_text('idf_component_register()')
  69. (component1dir / 'component1.h').touch()
  70. def test_component_requirements(self) -> None:
  71. # The main component uses component1.h, but this header is not in component1 public
  72. # interface. Hints should suggest that component1.h should be added into INCLUDE_DIRS
  73. # of component1.
  74. output = run_idf(['app'], self.projectdir)
  75. self.assertIn('Missing "component1.h" file name found in the following component(s): component1(', output)
  76. # Based on previous hint the component1.h is added to INCLUDE_DIRS, but main still doesn't
  77. # have dependency on compoment1. Hints should suggest to add component1 into main component
  78. # PRIV_REQUIRES, because foo.h is not in main public interface.
  79. run_idf(['fullclean'], self.projectdir)
  80. component1cmake = self.projectdir / 'components' / 'component1' / 'CMakeLists.txt'
  81. component1cmake.write_text('idf_component_register(INCLUDE_DIRS ".")')
  82. output = run_idf(['app'], self.projectdir)
  83. self.assertIn('To fix this, add component1 to PRIV_REQUIRES list of idf_component_register call', output)
  84. # Add foo.h into main public interface. Now the hint should suggest to use
  85. # REQUIRES instead of PRIV_REQUIRES.
  86. run_idf(['fullclean'], self.projectdir)
  87. maincmake = self.projectdir / 'main' / 'CMakeLists.txt'
  88. maincmake.write_text(('idf_component_register(SRCS "foo.c" '
  89. 'REQUIRES esp_timer '
  90. 'INCLUDE_DIRS ".")'))
  91. output = run_idf(['app'], self.projectdir)
  92. self.assertIn('To fix this, add component1 to REQUIRES list of idf_component_register call', output)
  93. # Add component1 to REQUIRES as suggested by previous hint, but also
  94. # add esp_psram as private req for component1 and add esp_psram.h
  95. # to component1.h. Now the hint should report that esp_psram should
  96. # be moved from PRIV_REQUIRES to REQUIRES for component1.
  97. run_idf(['fullclean'], self.projectdir)
  98. maincmake.write_text(('idf_component_register(SRCS "foo.c" '
  99. 'REQUIRES esp_timer component1 '
  100. 'INCLUDE_DIRS ".")'))
  101. (self.projectdir / 'components' / 'component1' / 'component1.h').write_text('#include "esp_psram.h"')
  102. component1cmake.write_text('idf_component_register(INCLUDE_DIRS "." PRIV_REQUIRES esp_psram)')
  103. output = run_idf(['app'], self.projectdir)
  104. self.assertIn('To fix this, move esp_psram from PRIV_REQUIRES into REQUIRES', output)
  105. def tearDown(self) -> None:
  106. self.tmpdir.cleanup()
  107. class TestNestedModuleComponentRequirements(unittest.TestCase):
  108. def setUp(self) -> None:
  109. # Set up a nested component structure. The components directory contains
  110. # component1, which also contains foo project with main component.
  111. # components/component1/project/main
  112. # ^^^^^^^^^^ ^^^^
  113. # component nested component
  114. # Both components include esp_timer.h, but only component1 has esp_timer
  115. # in requirements.
  116. self.tmpdir = tempfile.TemporaryDirectory()
  117. self.tmpdirpath = Path(self.tmpdir.name)
  118. components = self.tmpdirpath / 'components'
  119. maindir = components / 'component1'
  120. maindir.mkdir(parents=True)
  121. (maindir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "component1.c" PRIV_REQUIRES esp_timer)')
  122. (maindir / 'component1.c').write_text('#include "esp_timer.h"')
  123. self.projectdir = maindir / 'project'
  124. self.projectdir.mkdir(parents=True)
  125. (self.projectdir / 'CMakeLists.txt').write_text((
  126. 'cmake_minimum_required(VERSION 3.16)\n'
  127. f'set(EXTRA_COMPONENT_DIRS "{components}")\n'
  128. 'set(COMPONENTS main)\n'
  129. 'include($ENV{IDF_PATH}/tools/cmake/project.cmake)\n'
  130. 'project(foo)'))
  131. maindir = self.projectdir / 'main'
  132. maindir.mkdir()
  133. (maindir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "foo.c" REQUIRES component1)')
  134. (maindir / 'foo.c').write_text('#include "esp_timer.h"\nvoid app_main(){}')
  135. def test_nested_component_requirements(self) -> None:
  136. # Verify that source component for a failed include is properly identified
  137. # when components are nested. The main component should be identified as the
  138. # real source, not the component1 component.
  139. output = run_idf(['app'], self.projectdir)
  140. self.assertNotIn('BUG: esp_timer.h found in component esp_timer which is already in the requirements list of component1', output)
  141. self.assertIn('To fix this, add esp_timer to PRIV_REQUIRES list of idf_component_register call', output)
  142. def tearDown(self) -> None:
  143. self.tmpdir.cleanup()
  144. class TestTrimmedModuleComponentRequirements(unittest.TestCase):
  145. def setUp(self) -> None:
  146. # Set up a dummy project with a trimmed down list of components and main component.
  147. # The main component includes "esp_http_client.h", but the esp_http_client
  148. # component is not added to main's requirements.
  149. self.tmpdir = tempfile.TemporaryDirectory()
  150. self.tmpdirpath = Path(self.tmpdir.name)
  151. self.projectdir = self.tmpdirpath / 'project'
  152. self.projectdir.mkdir(parents=True)
  153. (self.projectdir / 'CMakeLists.txt').write_text((
  154. 'cmake_minimum_required(VERSION 3.16)\n'
  155. 'set(COMPONENTS main)\n'
  156. 'include($ENV{IDF_PATH}/tools/cmake/project.cmake)\n'
  157. 'project(foo)'))
  158. maindir = self.projectdir / 'main'
  159. maindir.mkdir()
  160. (maindir / 'CMakeLists.txt').write_text('idf_component_register(SRCS "foo.c")')
  161. (maindir / 'foo.c').write_text('#include "esp_http_client.h"\nvoid app_main(){}')
  162. def test_trimmed_component_requirements(self) -> None:
  163. output = run_idf(['app'], self.projectdir)
  164. self.assertIn('To fix this, add esp_http_client to PRIV_REQUIRES list of idf_component_register call in', output)
  165. def tearDown(self) -> None:
  166. self.tmpdir.cleanup()
  167. if __name__ == '__main__':
  168. unittest.main()