test_hints.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  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. class TestHintModuleComponentRequirements(unittest.TestCase):
  41. def run_idf(self, args: List[str]) -> str:
  42. # Simple helper to run idf command and return it's stdout.
  43. cmd = [
  44. sys.executable,
  45. os.path.join(os.environ['IDF_PATH'], 'tools', 'idf.py')
  46. ]
  47. proc = run(cmd + args, capture_output=True, cwd=str(self.projectdir), text=True)
  48. return proc.stdout + proc.stderr
  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 = self.run_idf(['app'])
  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. self.run_idf(['fullclean'])
  80. component1cmake = self.projectdir / 'components' / 'component1' / 'CMakeLists.txt'
  81. component1cmake.write_text('idf_component_register(INCLUDE_DIRS ".")')
  82. output = self.run_idf(['app'])
  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. self.run_idf(['fullclean'])
  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 = self.run_idf(['app'])
  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. New the hint should report that esp_psram should
  96. # be moved from PRIV_REQUIRES to REQUIRES for component1.
  97. self.run_idf(['fullclean'])
  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 = self.run_idf(['app'])
  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. if __name__ == '__main__':
  108. unittest.main()