CIAssignUnitTest.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. """
  2. Command line tool to assign unit tests to CI test jobs.
  3. """
  4. import os
  5. import re
  6. import argparse
  7. import yaml
  8. try:
  9. from yaml import CLoader as Loader
  10. except ImportError:
  11. from yaml import Loader as Loader
  12. from tiny_test_fw.Utility import CIAssignTest
  13. class Group(CIAssignTest.Group):
  14. SORT_KEYS = ["test environment", "tags", "chip_target"]
  15. MAX_CASE = 50
  16. ATTR_CONVERT_TABLE = {
  17. "execution_time": "execution time"
  18. }
  19. CI_JOB_MATCH_KEYS = ["test environment"]
  20. DUT_CLS_NAME = {
  21. "esp32": "ESP32DUT",
  22. "esp32s2": "ESP32S2DUT",
  23. "esp8266": "ESP8266DUT",
  24. }
  25. def __init__(self, case):
  26. super(Group, self).__init__(case)
  27. for tag in self._get_case_attr(case, "tags"):
  28. self.ci_job_match_keys.add(tag)
  29. @staticmethod
  30. def _get_case_attr(case, attr):
  31. if attr in Group.ATTR_CONVERT_TABLE:
  32. attr = Group.ATTR_CONVERT_TABLE[attr]
  33. return case[attr]
  34. def add_extra_case(self, case):
  35. """ If current group contains all tags required by case, then add succeed """
  36. added = False
  37. if self.accept_new_case():
  38. for key in self.filters:
  39. if self._get_case_attr(case, key) != self.filters[key]:
  40. if key == "tags":
  41. if set(self._get_case_attr(case, key)).issubset(set(self.filters[key])):
  42. continue
  43. break
  44. else:
  45. self.case_list.append(case)
  46. added = True
  47. return added
  48. def _create_extra_data(self, test_cases, test_function):
  49. """
  50. For unit test case, we need to copy some attributes of test cases into config file.
  51. So unit test function knows how to run the case.
  52. """
  53. case_data = []
  54. for case in test_cases:
  55. one_case_data = {
  56. "config": self._get_case_attr(case, "config"),
  57. "name": self._get_case_attr(case, "summary"),
  58. "reset": self._get_case_attr(case, "reset"),
  59. "timeout": self._get_case_attr(case, "timeout"),
  60. }
  61. if test_function in ["run_multiple_devices_cases", "run_multiple_stage_cases"]:
  62. try:
  63. one_case_data["child case num"] = self._get_case_attr(case, "child case num")
  64. except KeyError as e:
  65. print("multiple devices/stages cases must contains at least two test functions")
  66. print("case name: {}".format(one_case_data["name"]))
  67. raise e
  68. case_data.append(one_case_data)
  69. return case_data
  70. def _divide_case_by_test_function(self):
  71. """
  72. divide cases of current test group by test function they need to use
  73. :return: dict of list of cases for each test functions
  74. """
  75. case_by_test_function = {
  76. "run_multiple_devices_cases": [],
  77. "run_multiple_stage_cases": [],
  78. "run_unit_test_cases": [],
  79. }
  80. for case in self.case_list:
  81. if case["multi_device"] == "Yes":
  82. case_by_test_function["run_multiple_devices_cases"].append(case)
  83. elif case["multi_stage"] == "Yes":
  84. case_by_test_function["run_multiple_stage_cases"].append(case)
  85. else:
  86. case_by_test_function["run_unit_test_cases"].append(case)
  87. return case_by_test_function
  88. def output(self):
  89. """
  90. output data for job configs
  91. :return: {"Filter": case filter, "CaseConfig": list of case configs for cases in this group}
  92. """
  93. target = self._get_case_attr(self.case_list[0], "chip_target")
  94. if target:
  95. overwrite = {
  96. "dut": {
  97. "package": "ttfw_idf",
  98. "class": self.DUT_CLS_NAME[target],
  99. }
  100. }
  101. else:
  102. overwrite = dict()
  103. case_by_test_function = self._divide_case_by_test_function()
  104. output_data = {
  105. # we don't need filter for test function, as UT uses a few test functions for all cases
  106. "CaseConfig": [
  107. {
  108. "name": test_function,
  109. "extra_data": self._create_extra_data(test_cases, test_function),
  110. "overwrite": overwrite,
  111. } for test_function, test_cases in case_by_test_function.items() if test_cases
  112. ],
  113. }
  114. return output_data
  115. class UnitTestAssignTest(CIAssignTest.AssignTest):
  116. CI_TEST_JOB_PATTERN = re.compile(r"^UT_.+")
  117. def __init__(self, test_case_path, ci_config_file):
  118. CIAssignTest.AssignTest.__init__(self, test_case_path, ci_config_file, case_group=Group)
  119. def _search_cases(self, test_case_path, case_filter=None, test_case_file_pattern=None):
  120. """
  121. For unit test case, we don't search for test functions.
  122. The unit test cases is stored in a yaml file which is created in job build-idf-test.
  123. """
  124. def find_by_suffix(suffix, path):
  125. res = []
  126. for root, _, files in os.walk(path):
  127. for file in files:
  128. if file.endswith(suffix):
  129. res.append(os.path.join(root, file))
  130. return res
  131. def get_test_cases_from_yml(yml_file):
  132. try:
  133. with open(yml_file) as fr:
  134. raw_data = yaml.load(fr, Loader=Loader)
  135. test_cases = raw_data['test cases']
  136. except (IOError, KeyError):
  137. return []
  138. else:
  139. return test_cases
  140. test_cases = []
  141. if os.path.isdir(test_case_path):
  142. for yml_file in find_by_suffix('.yml', test_case_path):
  143. test_cases.extend(get_test_cases_from_yml(yml_file))
  144. elif os.path.isfile(test_case_path):
  145. test_cases.extend(get_test_cases_from_yml(test_case_path))
  146. else:
  147. print("Test case path is invalid. Should only happen when use @bot to skip unit test.")
  148. # filter keys are lower case. Do map lower case keys with original keys.
  149. try:
  150. key_mapping = {x.lower(): x for x in test_cases[0].keys()}
  151. except IndexError:
  152. key_mapping = dict()
  153. if case_filter:
  154. for key in case_filter:
  155. filtered_cases = []
  156. for case in test_cases:
  157. try:
  158. mapped_key = key_mapping[key]
  159. # bot converts string to lower case
  160. if isinstance(case[mapped_key], str):
  161. _value = case[mapped_key].lower()
  162. else:
  163. _value = case[mapped_key]
  164. if _value in case_filter[key]:
  165. filtered_cases.append(case)
  166. except KeyError:
  167. # case don't have this key, regard as filter success
  168. filtered_cases.append(case)
  169. test_cases = filtered_cases
  170. # sort cases with configs and test functions
  171. # in later stage cases with similar attributes are more likely to be assigned to the same job
  172. # it will reduce the count of flash DUT operations
  173. test_cases.sort(key=lambda x: x["config"] + x["multi_stage"] + x["multi_device"])
  174. return test_cases
  175. if __name__ == '__main__':
  176. parser = argparse.ArgumentParser()
  177. parser.add_argument("test_case",
  178. help="test case folder or file")
  179. parser.add_argument("ci_config_file",
  180. help="gitlab ci config file")
  181. parser.add_argument("output_path",
  182. help="output path of config files")
  183. args = parser.parse_args()
  184. assign_test = UnitTestAssignTest(args.test_case, args.ci_config_file)
  185. assign_test.assign_cases()
  186. assign_test.output_configs(args.output_path)