CIAssignTest.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. # Copyright 2015-2017 Espressif Systems (Shanghai) PTE LTD
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http:#www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """
  15. Common logic to assign test cases to CI jobs.
  16. Some background knowledge about Gitlab CI and use flow in esp-idf:
  17. * Gitlab CI jobs are static in ``.gitlab-ci.yml``. We can't dynamically create test jobs
  18. * For test job running on DUT, we use ``tags`` to select runners with different test environment
  19. * We have ``assign_test`` stage, will collect cases, and then assign them to correct test jobs
  20. * ``assign_test`` will fail if failed to assign any cases
  21. * with ``assign_test``, we can:
  22. * dynamically filter test case we want to test
  23. * alert user if they forget to add CI jobs and guide how to add test jobs
  24. * the last step of ``assign_test`` is to output config files, then test jobs will run these cases
  25. The Basic logic to assign test cases is as follow:
  26. 1. do search all the cases
  27. 2. do filter case (if filter is specified by @bot)
  28. 3. put cases to different groups according to rule of ``Group``
  29. * try to put them in existed groups
  30. * if failed then create a new group and add this case
  31. 4. parse and filter the test jobs from CI config file
  32. 5. try to assign all groups to jobs according to tags
  33. 6. output config files for jobs
  34. """
  35. import os
  36. import re
  37. import json
  38. import yaml
  39. from Utility import (CaseConfig, SearchCases, GitlabCIJob)
  40. class Group(object):
  41. MAX_EXECUTION_TIME = 30
  42. MAX_CASE = 15
  43. SORT_KEYS = ["env_tag"]
  44. def __init__(self, case):
  45. self.execution_time = 0
  46. self.case_list = [case]
  47. self.filters = dict(zip(self.SORT_KEYS, [self._get_case_attr(case, x) for x in self.SORT_KEYS]))
  48. @staticmethod
  49. def _get_case_attr(case, attr):
  50. # we might use different type for case (dict or test_func)
  51. # this method will do get attribute form cases
  52. return case.case_info[attr]
  53. def accept_new_case(self):
  54. """
  55. check if allowed to add any case to this group
  56. :return: True or False
  57. """
  58. max_time = (sum([self._get_case_attr(x, "execution_time") for x in self.case_list])
  59. < self.MAX_EXECUTION_TIME)
  60. max_case = (len(self.case_list) < self.MAX_CASE)
  61. return max_time and max_case
  62. def add_case(self, case):
  63. """
  64. add case to current group
  65. :param case: test case
  66. :return: True if add succeed, else False
  67. """
  68. added = False
  69. if self.accept_new_case():
  70. for key in self.filters:
  71. if self._get_case_attr(case, key) != self.filters[key]:
  72. break
  73. else:
  74. self.case_list.append(case)
  75. added = True
  76. return added
  77. def output(self):
  78. """
  79. output data for job configs
  80. :return: {"Filter": case filter, "CaseConfig": list of case configs for cases in this group}
  81. """
  82. output_data = {
  83. "Filter": self.filters,
  84. "CaseConfig": [{"name": self._get_case_attr(x, "name")} for x in self.case_list],
  85. }
  86. return output_data
  87. class AssignTest(object):
  88. """
  89. Auto assign tests to CI jobs.
  90. :param test_case_path: path of test case file(s)
  91. :param ci_config_file: path of ``.gitlab-ci.yml``
  92. """
  93. # subclass need to rewrite CI test job pattern, to filter all test jobs
  94. CI_TEST_JOB_PATTERN = re.compile(r"^test_.+")
  95. def __init__(self, test_case_path, ci_config_file, case_group=Group):
  96. self.test_case_path = test_case_path
  97. self.test_cases = []
  98. self.jobs = self._parse_gitlab_ci_config(ci_config_file)
  99. self.case_group = case_group
  100. def _parse_gitlab_ci_config(self, ci_config_file):
  101. with open(ci_config_file, "r") as f:
  102. ci_config = yaml.load(f)
  103. job_list = list()
  104. for job_name in ci_config:
  105. if self.CI_TEST_JOB_PATTERN.search(job_name) is not None:
  106. job_list.append(GitlabCIJob.Job(ci_config[job_name], job_name))
  107. return job_list
  108. @staticmethod
  109. def _search_cases(test_case_path, case_filter=None):
  110. """
  111. :param test_case_path: path contains test case folder
  112. :param case_filter: filter for test cases
  113. :return: filtered test case list
  114. """
  115. test_methods = SearchCases.Search.search_test_cases(test_case_path)
  116. return CaseConfig.filter_test_cases(test_methods, case_filter if case_filter else dict())
  117. def _group_cases(self):
  118. """
  119. separate all cases into groups according group rules. each group will be executed by one CI job.
  120. :return: test case groups.
  121. """
  122. groups = []
  123. for case in self.test_cases:
  124. for group in groups:
  125. # add to current group
  126. if group.add_case(case):
  127. break
  128. else:
  129. # create new group
  130. groups.append(self.case_group(case))
  131. return groups
  132. @staticmethod
  133. def _apply_bot_filter():
  134. """
  135. we support customize CI test with bot.
  136. here we process from and return the filter which ``_search_cases`` accepts.
  137. :return: filter for search test cases
  138. """
  139. bot_filter = os.getenv("BOT_CASE_FILTER")
  140. if bot_filter:
  141. bot_filter = json.loads(bot_filter)
  142. else:
  143. bot_filter = dict()
  144. return bot_filter
  145. def assign_cases(self):
  146. """
  147. separate test cases to groups and assign test cases to CI jobs.
  148. :raise AssertError: if failed to assign any case to CI job.
  149. :return: None
  150. """
  151. failed_to_assign = []
  152. case_filter = self._apply_bot_filter()
  153. self.test_cases = self._search_cases(self.test_case_path, case_filter)
  154. test_groups = self._group_cases()
  155. for group in test_groups:
  156. for job in self.jobs:
  157. if job.match_group(group):
  158. job.assign_group(group)
  159. break
  160. else:
  161. failed_to_assign.append(group)
  162. assert not failed_to_assign
  163. def output_configs(self, output_path):
  164. """
  165. :param output_path: path to output config files for each CI job
  166. :return: None
  167. """
  168. if not os.path.exists(output_path):
  169. os.makedirs(output_path)
  170. for job in self.jobs:
  171. job.output_config(output_path)