CaseConfig.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  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. Processing case config files.
  16. This is mainly designed for CI, we need to auto create and assign test jobs.
  17. Template Config File::
  18. TestConfig:
  19. app:
  20. package: ttfw_idf
  21. class: Example
  22. dut:
  23. path:
  24. class:
  25. config_file: /somewhere/config_file_for_runner
  26. test_name: CI_test_job_1
  27. Filter:
  28. chip: ESP32
  29. env_tag: default
  30. CaseConfig:
  31. - name: test_examples_protocol_https_request
  32. # optional
  33. extra_data: some extra data passed to case with kwarg extra_data
  34. overwrite: # overwrite test configs
  35. app:
  36. package: ttfw_idf
  37. class: Example
  38. - name: xxx
  39. """
  40. import importlib
  41. import yaml
  42. try:
  43. from yaml import CLoader as Loader
  44. except ImportError:
  45. from yaml import Loader as Loader
  46. from . import TestCase
  47. def _convert_to_lower_case_bytes(item):
  48. """
  49. bot filter is always lower case string.
  50. this function will convert to all string to lower case.
  51. Note: Unicode strings are converted to bytes.
  52. """
  53. if isinstance(item, (tuple, list)):
  54. output = [_convert_to_lower_case_bytes(v) for v in item]
  55. elif isinstance(item, type(b'')):
  56. output = item.lower()
  57. elif isinstance(item, type(u'')):
  58. output = item.encode().lower()
  59. else:
  60. output = item
  61. return output
  62. def _filter_one_case(test_method, case_filter):
  63. """ Apply filter for one case (the filter logic is the same as described in ``filter_test_cases``) """
  64. filter_result = True
  65. # filter keys are lower case. Do map lower case keys with original keys.
  66. key_mapping = {x.lower(): x for x in test_method.case_info.keys()}
  67. for orig_key in case_filter:
  68. key = key_mapping[orig_key]
  69. if key in test_method.case_info:
  70. # the filter key is both in case and filter
  71. # we need to check if they match
  72. filter_item = _convert_to_lower_case_bytes(case_filter[orig_key])
  73. accepted_item = _convert_to_lower_case_bytes(test_method.case_info[key])
  74. if isinstance(filter_item, (tuple, list)) \
  75. and isinstance(accepted_item, (tuple, list)):
  76. # both list/tuple, check if they have common item
  77. filter_result = True if set(filter_item) & set(accepted_item) else False
  78. elif isinstance(filter_item, (tuple, list)):
  79. # filter item list/tuple, check if case accepted value in filter item list/tuple
  80. filter_result = True if accepted_item in filter_item else False
  81. elif isinstance(accepted_item, (tuple, list)):
  82. # accepted item list/tuple, check if case filter value is in accept item list/tuple
  83. filter_result = True if filter_item in accepted_item else False
  84. else:
  85. if type(filter_item) != type(accepted_item):
  86. # This will catch silent ignores of test cases when Unicode and bytes are compared
  87. raise AssertionError(filter_item, '!=', accepted_item)
  88. # both string/int, just do string compare
  89. filter_result = (filter_item == accepted_item)
  90. else:
  91. # key in filter only, which means the case supports all values for this filter key, match succeed
  92. pass
  93. if not filter_result:
  94. # match failed
  95. break
  96. return filter_result
  97. def filter_test_cases(test_methods, case_filter):
  98. """
  99. filter test case. filter logic:
  100. 1. if filter key both in case attribute and filter:
  101. * if both value is string/int, then directly compare
  102. * if one is list/tuple, the other one is string/int, then check if string/int is in list/tuple
  103. * if both are list/tuple, then check if they have common item
  104. 2. if only case attribute or filter have the key, filter succeed
  105. 3. will do case insensitive compare for string
  106. for example, the following are match succeed scenarios
  107. (the rule is symmetric, result is same if exchange values for user filter and case attribute):
  108. * user case filter is ``chip: ["esp32", "esp32c"]``, case doesn't have ``chip`` attribute
  109. * user case filter is ``chip: ["esp32", "esp32c"]``, case attribute is ``chip: "esp32"``
  110. * user case filter is ``chip: "esp32"``, case attribute is ``chip: "esp32"``
  111. :param test_methods: a list of test methods functions
  112. :param case_filter: case filter
  113. :return: filtered test methods
  114. """
  115. filtered_test_methods = []
  116. for test_method in test_methods:
  117. if _filter_one_case(test_method, case_filter):
  118. filtered_test_methods.append(test_method)
  119. return filtered_test_methods
  120. class Parser(object):
  121. DEFAULT_CONFIG = {
  122. 'TestConfig': dict(),
  123. 'Filter': dict(),
  124. 'CaseConfig': [{'extra_data': None}],
  125. }
  126. @classmethod
  127. def parse_config_file(cls, config_file):
  128. """
  129. parse from config file and then update to default config.
  130. :param config_file: config file path
  131. :return: configs
  132. """
  133. configs = cls.DEFAULT_CONFIG.copy()
  134. if config_file:
  135. with open(config_file, 'r') as f:
  136. configs.update(yaml.load(f, Loader=Loader))
  137. return configs
  138. @classmethod
  139. def handle_overwrite_args(cls, overwrite):
  140. """
  141. handle overwrite configs. import module from path and then get the required class.
  142. :param overwrite: overwrite args
  143. :return: dict of (original key: class)
  144. """
  145. output = dict()
  146. for key in overwrite:
  147. module = importlib.import_module(overwrite[key]['package'])
  148. output[key] = module.__getattribute__(overwrite[key]['class'])
  149. return output
  150. @classmethod
  151. def apply_config(cls, test_methods, config_file):
  152. """
  153. apply config for test methods
  154. :param test_methods: a list of test methods functions
  155. :param config_file: case filter file
  156. :return: filtered cases
  157. """
  158. configs = cls.parse_config_file(config_file)
  159. test_case_list = []
  160. for _config in configs['CaseConfig']:
  161. _filter = configs['Filter'].copy()
  162. _overwrite = cls.handle_overwrite_args(_config.pop('overwrite', dict()))
  163. _extra_data = _config.pop('extra_data', None)
  164. _filter.update(_config)
  165. # Try get target from yml
  166. try:
  167. _target = _filter['target']
  168. except KeyError:
  169. _target = None
  170. else:
  171. _overwrite.update({'target': _target})
  172. for test_method in test_methods:
  173. if _filter_one_case(test_method, _filter):
  174. try:
  175. dut_dict = test_method.case_info['dut_dict']
  176. except (AttributeError, KeyError):
  177. dut_dict = None
  178. if dut_dict and _target:
  179. dut = test_method.case_info.get('dut')
  180. if _target.upper() in dut_dict:
  181. if dut and dut in dut_dict.values(): # don't overwrite special cases
  182. _overwrite.update({'dut': dut_dict[_target.upper()]})
  183. else:
  184. raise ValueError('target {} is not in the specified dut_dict'.format(_target))
  185. test_case_list.append(TestCase.TestCase(test_method, _extra_data, **_overwrite))
  186. return test_case_list
  187. class Generator(object):
  188. """ Case config file generator """
  189. def __init__(self):
  190. self.default_config = {
  191. 'TestConfig': dict(),
  192. 'Filter': dict(),
  193. }
  194. def set_default_configs(self, test_config, case_filter):
  195. """
  196. :param test_config: "TestConfig" value
  197. :param case_filter: "Filter" value
  198. :return: None
  199. """
  200. self.default_config = {'TestConfig': test_config, 'Filter': case_filter}
  201. def generate_config(self, case_configs, output_file):
  202. """
  203. :param case_configs: "CaseConfig" value
  204. :param output_file: output file path
  205. :return: None
  206. """
  207. config = self.default_config.copy()
  208. config.update({'CaseConfig': case_configs})
  209. with open(output_file, 'w') as f:
  210. yaml.dump(config, f)