native_tests.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. #!/usr/bin/env python3
  2. import os
  3. import subprocess
  4. import sys
  5. from lib.util import SRC_DIR
  6. PYYAML_LIB_DIR = os.path.join(SRC_DIR, 'third_party', 'pyyaml', 'lib')
  7. sys.path.append(PYYAML_LIB_DIR)
  8. import yaml #pylint: disable=wrong-import-position,wrong-import-order
  9. class Verbosity:
  10. CHATTY = 'chatty' # stdout and stderr
  11. ERRORS = 'errors' # stderr only
  12. SILENT = 'silent' # no output
  13. @staticmethod
  14. def get_all():
  15. return Verbosity.__get_all_in_order()
  16. @staticmethod
  17. def __get_all_in_order():
  18. return [Verbosity.SILENT, Verbosity.ERRORS, Verbosity.CHATTY]
  19. @staticmethod
  20. def __get_indices(*values):
  21. ordered = Verbosity.__get_all_in_order()
  22. indices = map(ordered.index, values)
  23. return indices
  24. @staticmethod
  25. def ge(a, b):
  26. """Greater or equal"""
  27. a_index, b_index = Verbosity.__get_indices(a, b)
  28. return a_index >= b_index
  29. @staticmethod
  30. def le(a, b):
  31. """Less or equal"""
  32. a_index, b_index = Verbosity.__get_indices(a, b)
  33. return a_index <= b_index
  34. class DisabledTestsPolicy:
  35. DISABLE = 'disable' # Disabled tests are disabled. Wow. Much sense.
  36. ONLY = 'only' # Only disabled tests should be run.
  37. INCLUDE = 'include' # Do not disable any tests.
  38. class Platform:
  39. LINUX = 'linux'
  40. MAC = 'mac'
  41. WINDOWS = 'windows'
  42. @staticmethod
  43. def get_current():
  44. platform = sys.platform
  45. if platform in ('linux', 'linux2'):
  46. return Platform.LINUX
  47. if platform == 'darwin':
  48. return Platform.MAC
  49. if platform in ('cygwin', 'win32'):
  50. return Platform.WINDOWS
  51. raise AssertionError(
  52. f"unexpected current platform '{platform}'")
  53. @staticmethod
  54. def get_all():
  55. return [Platform.LINUX, Platform.MAC, Platform.WINDOWS]
  56. @staticmethod
  57. def is_valid(platform):
  58. return platform in Platform.get_all()
  59. class TestsList():
  60. def __init__(self, config_path, tests_dir):
  61. self.config_path = config_path
  62. self.tests_dir = tests_dir
  63. # A dict with binary names (e.g. 'base_unittests') as keys
  64. # and various test data as values of dict type.
  65. self.tests = TestsList.__get_tests_list(config_path)
  66. def __len__(self):
  67. return len(self.tests)
  68. def get_for_current_platform(self):
  69. all_binaries = self.tests.keys()
  70. supported_binaries = filter(self.__platform_supports, all_binaries)
  71. return supported_binaries
  72. def run(self, binaries, output_dir=None, verbosity=Verbosity.CHATTY,
  73. disabled_tests_policy=DisabledTestsPolicy.DISABLE):
  74. # Don't run anything twice.
  75. binaries = set(binaries)
  76. # First check that all names are present in the config.
  77. for binary_name in binaries:
  78. if binary_name not in self.tests:
  79. msg = f"binary {binary_name} not found in config '{self.config_path}'"
  80. raise Exception(msg)
  81. # Respect the "platform" setting.
  82. for binary_name in binaries:
  83. if not self.__platform_supports(binary_name):
  84. host = Platform.get_current()
  85. errmsg = f"binary {binary_name} cannot run on {host}. Check the config"
  86. raise Exception(errmsg)
  87. suite_returncode = sum(
  88. self.__run(binary, output_dir, verbosity, disabled_tests_policy)
  89. for binary in binaries)
  90. return suite_returncode
  91. def run_all(self, output_dir=None, verbosity=Verbosity.CHATTY,
  92. disabled_tests_policy=DisabledTestsPolicy.DISABLE):
  93. return self.run(self.get_for_current_platform(), output_dir, verbosity,
  94. disabled_tests_policy)
  95. @staticmethod
  96. def __get_tests_list(config_path):
  97. tests_list = {}
  98. config_data = TestsList.__get_config_data(config_path)
  99. for data_item in config_data['tests']:
  100. (binary_name, test_data) = TestsList.__get_test_data(data_item)
  101. tests_list[binary_name] = test_data
  102. return tests_list
  103. @staticmethod
  104. def __get_config_data(config_path):
  105. with open(config_path, 'r', encoding='utf-8') as stream:
  106. return yaml.load(stream)
  107. @staticmethod
  108. def __expand_shorthand(value):
  109. """ Treat a string as {'string_value': None}."""
  110. if isinstance(value, dict):
  111. return value
  112. if isinstance(value, str):
  113. return {value: None}
  114. raise AssertionError(f"unexpected shorthand type: {type(value)}")
  115. @staticmethod
  116. def __make_a_list(value):
  117. """Make a list if not already a list."""
  118. if isinstance(value, list):
  119. return value
  120. return [value]
  121. @staticmethod
  122. def __merge_nested_lists(value):
  123. """Converts a dict of lists to a list."""
  124. if isinstance(value, list):
  125. return value
  126. if isinstance(value, dict):
  127. # It looks ugly as hell, but it does the job.
  128. return [list_item for key in value for list_item in value[key]]
  129. raise AssertionError(
  130. f"unexpected type for list merging: {type(value)}")
  131. def __platform_supports(self, binary_name):
  132. return Platform.get_current() in self.tests[binary_name]['platforms']
  133. @staticmethod
  134. def __get_test_data(data_item):
  135. data_item = TestsList.__expand_shorthand(data_item)
  136. binary_name = data_item.keys()[0]
  137. test_data = {
  138. 'excluded_tests': [],
  139. 'platforms': Platform.get_all()
  140. }
  141. configs = data_item[binary_name]
  142. if configs is not None:
  143. # List of excluded tests.
  144. if 'disabled' in configs:
  145. excluded_tests = TestsList.__merge_nested_lists(configs['disabled'])
  146. test_data['excluded_tests'] = excluded_tests
  147. # List of platforms to run the tests on.
  148. if 'platform' in configs:
  149. platforms = TestsList.__make_a_list(configs['platform'])
  150. for platform in platforms:
  151. assert Platform.is_valid(platform), \
  152. f"Unsupported platform {platform}, check {binary_name} config"
  153. test_data['platforms'] = platforms
  154. return (binary_name, test_data)
  155. def __run(self, binary_name, output_dir, verbosity,
  156. disabled_tests_policy):
  157. binary_path = os.path.join(self.tests_dir, binary_name)
  158. test_binary = TestBinary(binary_path)
  159. test_data = self.tests[binary_name]
  160. included_tests = []
  161. excluded_tests = test_data['excluded_tests']
  162. if disabled_tests_policy == DisabledTestsPolicy.ONLY:
  163. if len(excluded_tests) == 0:
  164. # There is nothing to run.
  165. return 0
  166. included_tests, excluded_tests = excluded_tests, included_tests
  167. if disabled_tests_policy == DisabledTestsPolicy.INCLUDE:
  168. excluded_tests = []
  169. output_file_path = TestsList.__get_output_path(binary_name, output_dir)
  170. return test_binary.run(included_tests=included_tests,
  171. excluded_tests=excluded_tests,
  172. output_file_path=output_file_path,
  173. verbosity=verbosity)
  174. @staticmethod
  175. def __get_output_path(binary_name, output_dir=None):
  176. if output_dir is None:
  177. return None
  178. return os.path.join(output_dir, f"results_{binary_name}.xml")
  179. class TestBinary():
  180. # Is only used when writing to a file.
  181. output_format = 'xml'
  182. def __init__(self, binary_path):
  183. self.binary_path = binary_path
  184. def run(self, included_tests=None, excluded_tests=None,
  185. output_file_path=None, verbosity=Verbosity.CHATTY):
  186. gtest_filter = TestBinary.__get_gtest_filter(included_tests,
  187. excluded_tests)
  188. gtest_output = TestBinary.__get_gtest_output(output_file_path)
  189. args = [self.binary_path, gtest_filter, gtest_output]
  190. returncode = 0
  191. with open(os.devnull, "w", encoding='utf-8') as devnull:
  192. stdout = stderr = None
  193. if Verbosity.le(verbosity, Verbosity.ERRORS):
  194. stdout = devnull
  195. if verbosity == Verbosity.SILENT:
  196. stderr = devnull
  197. try:
  198. returncode = subprocess.call(args, stdout=stdout, stderr=stderr)
  199. except Exception as exception:
  200. if Verbosity.ge(verbosity, Verbosity.ERRORS):
  201. print(f"An error occurred while running '{self.binary_path}':",
  202. '\n', exception, file=sys.stderr)
  203. returncode = 1
  204. return returncode
  205. @staticmethod
  206. def __get_gtest_filter(included_tests, excluded_tests):
  207. included_str = TestBinary.__list_tests(included_tests)
  208. excluded_str = TestBinary.__list_tests(excluded_tests)
  209. return f"--gtest_filter={included_str}-{excluded_str}"
  210. @staticmethod
  211. def __get_gtest_output(output_file_path):
  212. if output_file_path is None:
  213. return ""
  214. return f"--gtest_output={TestBinary.output_format}:{output_file_path}"
  215. @staticmethod
  216. def __list_tests(tests):
  217. if tests is None:
  218. return ''
  219. return ':'.join(tests)