CIScanTests.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import argparse
  2. import errno
  3. import json
  4. import logging
  5. import os
  6. from collections import defaultdict
  7. from copy import deepcopy
  8. from typing import Any
  9. from ci.idf_ci_utils import get_pytest_app_paths
  10. from find_apps import find_apps, find_builds_for_app
  11. from find_build_apps import BUILD_SYSTEM_CMAKE, BUILD_SYSTEMS, config_rules_from_str
  12. from idf_py_actions.constants import PREVIEW_TARGETS, SUPPORTED_TARGETS
  13. from ttfw_idf.IDFAssignTest import ExampleAssignTest, TestAppsAssignTest
  14. TEST_LABELS = {
  15. 'example_test': 'BOT_LABEL_EXAMPLE_TEST',
  16. 'test_apps': 'BOT_LABEL_CUSTOM_TEST',
  17. 'component_ut': ['BOT_LABEL_UNIT_TEST',
  18. 'BOT_LABEL_UNIT_TEST_32',
  19. 'BOT_LABEL_UNIT_TEST_S2',
  20. 'BOT_LABEL_UNIT_TEST_C3'],
  21. }
  22. BUILD_ALL_LABELS = [
  23. 'BOT_LABEL_BUILD',
  24. 'BOT_LABEL_BUILD_ALL_APPS',
  25. 'BOT_LABEL_REGULAR_TEST',
  26. 'BOT_LABEL_WEEKEND_TEST',
  27. 'NIGHTLY_RUN',
  28. 'BOT_LABEL_NIGHTLY_RUN',
  29. ]
  30. BUILD_PER_JOB = 30 # each build takes 1 mins around
  31. def _has_build_all_label(): # type: () -> bool
  32. for label in BUILD_ALL_LABELS:
  33. if os.getenv(label):
  34. return True
  35. return False
  36. def _judge_build_or_not(action, build_all): # type: (str, bool) -> tuple[bool, bool]
  37. """
  38. :return: (build_or_not_for_test_related_apps, build_or_not_for_non_related_apps)
  39. """
  40. if build_all or _has_build_all_label() or (not os.getenv('BOT_TRIGGER_WITH_LABEL')):
  41. logging.info('Build all apps')
  42. return True, True
  43. labels = TEST_LABELS[action]
  44. if not isinstance(labels, list):
  45. labels = [labels] # type: ignore
  46. for label in labels:
  47. if os.getenv(label):
  48. logging.info('Build only test cases apps')
  49. return True, False
  50. logging.info('Skip all')
  51. return False, False
  52. def output_json(apps_dict_list, target, build_system, output_dir): # type: (list, str, str, str) -> None
  53. output_path = os.path.join(output_dir, 'scan_{}_{}.json'.format(target.lower(), build_system))
  54. with open(output_path, 'w') as fw:
  55. fw.writelines([json.dumps(app) + '\n' for app in apps_dict_list])
  56. # we might need artifacts to run test cases locally.
  57. # So we need to save artifacts which have test case not executed by CI.
  58. class _ExampleAssignTest(ExampleAssignTest):
  59. DEFAULT_FILTER = {} # type: dict[str, Any]
  60. class _TestAppsAssignTest(TestAppsAssignTest):
  61. DEFAULT_FILTER = {} # type: dict[str, Any]
  62. def main(): # type: () -> None
  63. parser = argparse.ArgumentParser(description='Scan the required build tests')
  64. parser.add_argument('test_type',
  65. choices=TEST_LABELS.keys(),
  66. help='Scan test type')
  67. parser.add_argument('paths', nargs='+',
  68. help='One or more app paths')
  69. parser.add_argument('-b', '--build-system',
  70. choices=BUILD_SYSTEMS.keys(),
  71. default=BUILD_SYSTEM_CMAKE)
  72. parser.add_argument('-c', '--ci-config-file',
  73. required=True,
  74. help='gitlab ci config target-test file')
  75. parser.add_argument('-o', '--output-path',
  76. required=True,
  77. help='output path of the scan result')
  78. parser.add_argument('--exclude', nargs='*',
  79. help='Ignore specified directory. Can be used multiple times.')
  80. parser.add_argument('--extra_test_dirs', nargs='*',
  81. help='Additional directories to preserve artifacts for local tests')
  82. parser.add_argument('--preserve_all', action='store_true',
  83. help='add this flag to preserve artifacts for all apps')
  84. parser.add_argument('--build-all', action='store_true',
  85. help='add this flag to build all apps')
  86. parser.add_argument('--combine-all-targets', action='store_true',
  87. help='add this flag to combine all target jsons into one')
  88. parser.add_argument('--except-targets', nargs='+',
  89. help='only useful when "--combine-all-targets". Specified targets would be skipped.')
  90. parser.add_argument(
  91. '--config',
  92. action='append',
  93. help='Only useful when "--evaluate-parallel-count" is flagged.'
  94. 'Adds configurations (sdkconfig file names) to build. This can either be ' +
  95. 'FILENAME[=NAME] or FILEPATTERN. FILENAME is the name of the sdkconfig file, ' +
  96. 'relative to the project directory, to be used. Optional NAME can be specified, ' +
  97. 'which can be used as a name of this configuration. FILEPATTERN is the name of ' +
  98. 'the sdkconfig file, relative to the project directory, with at most one wildcard. ' +
  99. 'The part captured by the wildcard is used as the name of the configuration.',
  100. )
  101. parser.add_argument('--evaluate-parallel-count', action='store_true',
  102. help='suggest parallel count according to build items')
  103. args = parser.parse_args()
  104. build_test_case_apps, build_standalone_apps = _judge_build_or_not(args.test_type, args.build_all)
  105. if not os.path.exists(args.output_path):
  106. try:
  107. os.makedirs(args.output_path)
  108. except OSError as e:
  109. if e.errno != errno.EEXIST:
  110. raise e
  111. SUPPORTED_TARGETS.extend(PREVIEW_TARGETS)
  112. if (not build_standalone_apps) and (not build_test_case_apps):
  113. for target in SUPPORTED_TARGETS:
  114. output_json([], target, args.build_system, args.output_path)
  115. SystemExit(0)
  116. idf_path = str(os.getenv('IDF_PATH'))
  117. paths = set([os.path.join(idf_path, path) if not os.path.isabs(path) else path for path in args.paths])
  118. test_cases = []
  119. for path in paths:
  120. if args.test_type == 'example_test':
  121. assign = _ExampleAssignTest(path, args.ci_config_file)
  122. elif args.test_type in ['test_apps', 'component_ut']:
  123. assign = _TestAppsAssignTest(path, args.ci_config_file)
  124. else:
  125. raise SystemExit(1) # which is impossible
  126. test_cases.extend(assign.search_cases())
  127. '''
  128. {
  129. <target>: {
  130. 'test_case_apps': [<app_dir>], # which is used in target tests
  131. 'standalone_apps': [<app_dir>], # which is not
  132. },
  133. ...
  134. }
  135. '''
  136. scan_info_dict = defaultdict(dict) # type: dict[str, dict]
  137. # store the test cases dir, exclude these folders when scan for standalone apps
  138. default_exclude = args.exclude if args.exclude else []
  139. build_system = args.build_system.lower()
  140. build_system_class = BUILD_SYSTEMS[build_system]
  141. for target in SUPPORTED_TARGETS:
  142. exclude_apps = deepcopy(default_exclude)
  143. if build_test_case_apps:
  144. scan_info_dict[target]['test_case_apps'] = set()
  145. test_dirs = args.extra_test_dirs if args.extra_test_dirs else []
  146. for case in test_cases:
  147. if case.case_info['target'].lower() == target.lower():
  148. test_dirs.append(case.case_info['app_dir'])
  149. for app_dir in test_dirs:
  150. app_dir = os.path.join(idf_path, app_dir) if not os.path.isabs(app_dir) else app_dir
  151. _apps = find_apps(build_system_class, app_dir, True, exclude_apps, target.lower())
  152. if _apps:
  153. scan_info_dict[target]['test_case_apps'].update(_apps)
  154. exclude_apps.extend(_apps)
  155. else:
  156. scan_info_dict[target]['test_case_apps'] = set()
  157. if build_standalone_apps:
  158. scan_info_dict[target]['standalone_apps'] = set()
  159. for path in paths:
  160. scan_info_dict[target]['standalone_apps'].update(
  161. find_apps(build_system_class, path, True, exclude_apps, target.lower()))
  162. else:
  163. scan_info_dict[target]['standalone_apps'] = set()
  164. test_case_apps_preserve_default = True if build_system == 'cmake' else False
  165. output_files = []
  166. build_items_total_count = 0
  167. for target in SUPPORTED_TARGETS:
  168. # get pytest apps paths
  169. pytest_app_paths = set()
  170. for path in paths:
  171. pytest_app_paths.update(get_pytest_app_paths(path, target))
  172. apps = []
  173. for app_dir in scan_info_dict[target]['test_case_apps']:
  174. if app_dir in pytest_app_paths:
  175. print(f'WARNING: has pytest script: {app_dir}')
  176. continue
  177. apps.append({
  178. 'app_dir': app_dir,
  179. 'build_system': args.build_system,
  180. 'target': target,
  181. 'preserve': args.preserve_all or test_case_apps_preserve_default
  182. })
  183. for app_dir in scan_info_dict[target]['standalone_apps']:
  184. if app_dir in pytest_app_paths:
  185. print(f'Skipping pytest app: {app_dir}')
  186. continue
  187. apps.append({
  188. 'app_dir': app_dir,
  189. 'build_system': args.build_system,
  190. 'target': target,
  191. 'preserve': args.preserve_all
  192. })
  193. output_path = os.path.join(args.output_path, 'scan_{}_{}.json'.format(target.lower(), build_system))
  194. with open(output_path, 'w') as fw:
  195. if args.evaluate_parallel_count:
  196. build_items = []
  197. config_rules = config_rules_from_str(args.config or [])
  198. for app in apps:
  199. build_items += find_builds_for_app(
  200. app['app_dir'],
  201. app['app_dir'],
  202. 'build',
  203. '',
  204. app['target'],
  205. app['build_system'],
  206. config_rules,
  207. app['preserve'],
  208. )
  209. print('Found {} builds'.format(len(build_items)))
  210. if args.combine_all_targets:
  211. if (args.except_targets and target not in [t.lower() for t in args.except_targets]) \
  212. or (not args.except_targets):
  213. build_items_total_count += len(build_items)
  214. else:
  215. print(f'suggest set parallel count for target {target} to {len(build_items) // BUILD_PER_JOB + 1}')
  216. fw.writelines([json.dumps(app) + '\n' for app in apps])
  217. if args.combine_all_targets:
  218. if (args.except_targets and target not in [t.lower() for t in args.except_targets]) \
  219. or (not args.except_targets):
  220. output_files.append(output_path)
  221. else:
  222. print(f'skipping combining target {target}')
  223. if args.combine_all_targets:
  224. scan_all_json = os.path.join(args.output_path, f'scan_all_{build_system}.json')
  225. lines = []
  226. for file in output_files:
  227. with open(file) as fr:
  228. lines.extend([line for line in fr.readlines() if line.strip()])
  229. with open(scan_all_json, 'w') as fw:
  230. fw.writelines(lines)
  231. print(f'combined into file: {scan_all_json}')
  232. if args.evaluate_parallel_count:
  233. print(f'Total build: {build_items_total_count}. Suggest set parallel count for all target to {build_items_total_count // BUILD_PER_JOB + 1}')
  234. if __name__ == '__main__':
  235. main()