test_configuration.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  1. # Copyright (C) 2011 Google Inc. All rights reserved.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions are
  5. # met:
  6. #
  7. # * Redistributions of source code must retain the above copyright
  8. # notice, this list of conditions and the following disclaimer.
  9. # * Redistributions in binary form must reproduce the above
  10. # copyright notice, this list of conditions and the following disclaimer
  11. # in the documentation and/or other materials provided with the
  12. # distribution.
  13. # * Neither the Google name nor the names of its
  14. # contributors may be used to endorse or promote products derived from
  15. # this software without specific prior written permission.
  16. #
  17. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  18. # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  19. # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  20. # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  21. # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  22. # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  23. # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  24. # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  25. # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  27. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. class TestConfiguration(object):
  29. def __init__(self, version, architecture, build_type):
  30. self.version = version
  31. self.architecture = architecture
  32. self.build_type = build_type
  33. @classmethod
  34. def category_order(cls):
  35. """The most common human-readable order in which the configuration properties are listed."""
  36. return ['version', 'architecture', 'build_type']
  37. def items(self):
  38. return self.__dict__.items()
  39. def keys(self):
  40. return self.__dict__.keys()
  41. def __str__(self):
  42. return ("<%(version)s, %(architecture)s, %(build_type)s>" %
  43. self.__dict__)
  44. def __repr__(self):
  45. return "TestConfig(version='%(version)s', architecture='%(architecture)s', build_type='%(build_type)s')" % self.__dict__
  46. def __hash__(self):
  47. return hash(self.version + self.architecture + self.build_type)
  48. def __eq__(self, other):
  49. return self.__hash__() == other.__hash__()
  50. def values(self):
  51. """Returns the configuration values of this instance as a tuple."""
  52. return self.__dict__.values()
  53. class SpecifierSorter(object):
  54. def __init__(self, all_test_configurations=None, macros=None):
  55. self._specifier_to_category = {}
  56. if not all_test_configurations:
  57. return
  58. for test_configuration in all_test_configurations:
  59. for category, specifier in test_configuration.items():
  60. self.add_specifier(category, specifier)
  61. self.add_macros(macros)
  62. def add_specifier(self, category, specifier):
  63. self._specifier_to_category[specifier] = category
  64. def add_macros(self, macros):
  65. if not macros:
  66. return
  67. # Assume well-formed macros.
  68. for macro, specifier_list in macros.items():
  69. self.add_specifier(self.category_for_specifier(specifier_list[0]), macro)
  70. @classmethod
  71. def category_priority(cls, category):
  72. return TestConfiguration.category_order().index(category)
  73. def specifier_priority(self, specifier):
  74. return self.category_priority(self._specifier_to_category[specifier])
  75. def category_for_specifier(self, specifier):
  76. return self._specifier_to_category.get(specifier)
  77. def sort_specifiers(self, specifiers):
  78. category_slots = map(lambda x: [], TestConfiguration.category_order())
  79. for specifier in specifiers:
  80. category_slots[self.specifier_priority(specifier)].append(specifier)
  81. def sort_and_return(result, specifier_list):
  82. specifier_list.sort()
  83. return result + specifier_list
  84. return reduce(sort_and_return, category_slots, [])
  85. class TestConfigurationConverter(object):
  86. def __init__(self, all_test_configurations, configuration_macros=None):
  87. self._all_test_configurations = all_test_configurations
  88. self._configuration_macros = configuration_macros or {}
  89. self._specifier_to_configuration_set = {}
  90. self._specifier_sorter = SpecifierSorter()
  91. self._collapsing_sets_by_size = {}
  92. self._junk_specifier_combinations = {}
  93. self._collapsing_sets_by_category = {}
  94. matching_sets_by_category = {}
  95. for configuration in all_test_configurations:
  96. for category, specifier in configuration.items():
  97. self._specifier_to_configuration_set.setdefault(specifier, set()).add(configuration)
  98. self._specifier_sorter.add_specifier(category, specifier)
  99. self._collapsing_sets_by_category.setdefault(category, set()).add(specifier)
  100. # FIXME: This seems extra-awful.
  101. for cat2, spec2 in configuration.items():
  102. if category == cat2:
  103. continue
  104. matching_sets_by_category.setdefault(specifier, {}).setdefault(cat2, set()).add(spec2)
  105. for collapsing_set in self._collapsing_sets_by_category.values():
  106. self._collapsing_sets_by_size.setdefault(len(collapsing_set), set()).add(frozenset(collapsing_set))
  107. for specifier, sets_by_category in matching_sets_by_category.items():
  108. for category, set_by_category in sets_by_category.items():
  109. if len(set_by_category) == 1 and self._specifier_sorter.category_priority(category) > self._specifier_sorter.specifier_priority(specifier):
  110. self._junk_specifier_combinations[specifier] = set_by_category
  111. self._specifier_sorter.add_macros(configuration_macros)
  112. def specifier_sorter(self):
  113. return self._specifier_sorter
  114. def _expand_macros(self, specifier):
  115. expanded_specifiers = self._configuration_macros.get(specifier)
  116. return expanded_specifiers or [specifier]
  117. def to_config_set(self, specifier_set, error_list=None):
  118. """Convert a list of specifiers into a set of TestConfiguration instances."""
  119. if len(specifier_set) == 0:
  120. return self._all_test_configurations
  121. matching_sets = {}
  122. for specifier in specifier_set:
  123. for expanded_specifier in self._expand_macros(specifier):
  124. configurations = self._specifier_to_configuration_set.get(expanded_specifier)
  125. if not configurations:
  126. if error_list is not None:
  127. error_list.append("Unrecognized modifier '" + expanded_specifier + "'")
  128. return set()
  129. category = self._specifier_sorter.category_for_specifier(expanded_specifier)
  130. matching_sets.setdefault(category, set()).update(configurations)
  131. return reduce(set.intersection, matching_sets.values())
  132. @classmethod
  133. def collapse_macros(cls, macros_dict, specifiers_list):
  134. for macro_specifier, macro in macros_dict.items():
  135. if len(macro) == 1:
  136. continue
  137. for combination in cls.combinations(specifiers_list, len(macro)):
  138. if cls.symmetric_difference(combination) == set(macro):
  139. for item in combination:
  140. specifiers_list.remove(item)
  141. new_specifier_set = cls.intersect_combination(combination)
  142. new_specifier_set.add(macro_specifier)
  143. specifiers_list.append(frozenset(new_specifier_set))
  144. def collapse_individual_specifier_set(macro_specifier, macro):
  145. specifiers_to_remove = []
  146. specifiers_to_add = []
  147. for specifier_set in specifiers_list:
  148. macro_set = set(macro)
  149. if macro_set.intersection(specifier_set) == macro_set:
  150. specifiers_to_remove.append(specifier_set)
  151. specifiers_to_add.append(frozenset((set(specifier_set) - macro_set) | set([macro_specifier])))
  152. for specifier in specifiers_to_remove:
  153. specifiers_list.remove(specifier)
  154. for specifier in specifiers_to_add:
  155. specifiers_list.append(specifier)
  156. for macro_specifier, macro in macros_dict.items():
  157. collapse_individual_specifier_set(macro_specifier, macro)
  158. # FIXME: itertools.combinations in buggy in Python 2.6.1 (the version that ships on SL).
  159. # It seems to be okay in 2.6.5 or later; until then, this is the implementation given
  160. # in http://docs.python.org/library/itertools.html (from 2.7).
  161. @staticmethod
  162. def combinations(iterable, r):
  163. # combinations('ABCD', 2) --> AB AC AD BC BD CD
  164. # combinations(range(4), 3) --> 012 013 023 123
  165. pool = tuple(iterable)
  166. n = len(pool)
  167. if r > n:
  168. return
  169. indices = range(r)
  170. yield tuple(pool[i] for i in indices)
  171. while True:
  172. for i in reversed(range(r)):
  173. if indices[i] != i + n - r:
  174. break
  175. else:
  176. return
  177. indices[i] += 1 # pylint: disable=W0631
  178. for j in range(i + 1, r): # pylint: disable=W0631
  179. indices[j] = indices[j - 1] + 1
  180. yield tuple(pool[i] for i in indices)
  181. @classmethod
  182. def intersect_combination(cls, combination):
  183. return reduce(set.intersection, [set(specifiers) for specifiers in combination])
  184. @classmethod
  185. def symmetric_difference(cls, iterable):
  186. union = set()
  187. intersection = iterable[0]
  188. for item in iterable:
  189. union = union | item
  190. intersection = intersection.intersection(item)
  191. return union - intersection
  192. def to_specifiers_list(self, test_configuration_set):
  193. """Convert a set of TestConfiguration instances into one or more list of specifiers."""
  194. # Easy out: if the set is all configurations, the modifier is empty.
  195. if len(test_configuration_set) == len(self._all_test_configurations):
  196. return [[]]
  197. # 1) Build a list of specifier sets, discarding specifiers that don't add value.
  198. specifiers_list = []
  199. for config in test_configuration_set:
  200. values = set(config.values())
  201. for specifier, junk_specifier_set in self._junk_specifier_combinations.items():
  202. if specifier in values:
  203. values -= junk_specifier_set
  204. specifiers_list.append(frozenset(values))
  205. def try_collapsing(size, collapsing_sets):
  206. if len(specifiers_list) < size:
  207. return False
  208. for combination in self.combinations(specifiers_list, size):
  209. if self.symmetric_difference(combination) in collapsing_sets:
  210. for item in combination:
  211. specifiers_list.remove(item)
  212. specifiers_list.append(frozenset(self.intersect_combination(combination)))
  213. return True
  214. return False
  215. # 2) Collapse specifier sets with common specifiers:
  216. # (xp, release), (xp, debug) --> (xp, x86)
  217. for size, collapsing_sets in self._collapsing_sets_by_size.items():
  218. while try_collapsing(size, collapsing_sets):
  219. pass
  220. def try_abbreviating(collapsing_sets):
  221. if len(specifiers_list) < 2:
  222. return False
  223. for combination in self.combinations(specifiers_list, 2):
  224. for collapsing_set in collapsing_sets:
  225. diff = self.symmetric_difference(combination)
  226. if diff <= collapsing_set:
  227. common = self.intersect_combination(combination)
  228. for item in combination:
  229. specifiers_list.remove(item)
  230. specifiers_list.append(frozenset(common | diff))
  231. return True
  232. return False
  233. # 3) Abbreviate specifier sets by combining specifiers across categories.
  234. # (xp, release), (win7, release) --> (xp, win7, release)
  235. while try_abbreviating(self._collapsing_sets_by_size.values()):
  236. pass
  237. # 4) Substitute specifier subsets that match macros witin each set:
  238. # (xp, vista, win7, release) -> (win, release)
  239. self.collapse_macros(self._configuration_macros, specifiers_list)
  240. macro_keys = set(self._configuration_macros.keys())
  241. # 5) Collapsing macros may have created combinations the can now be abbreviated.
  242. # (xp, release), (linux, x86, release), (linux, x86_64, release) --> (xp, release), (linux, release) --> (xp, linux, release)
  243. while try_abbreviating([self._collapsing_sets_by_category['version'] | macro_keys]):
  244. pass
  245. # 6) Remove cases where we have collapsed but have all macros.
  246. # (android, win, mac, linux, release) --> (release)
  247. specifiers_to_remove = []
  248. for specifier_set in specifiers_list:
  249. if macro_keys <= specifier_set:
  250. specifiers_to_remove.append(specifier_set)
  251. for specifier_set in specifiers_to_remove:
  252. specifiers_list.remove(specifier_set)
  253. specifiers_list.append(frozenset(specifier_set - macro_keys))
  254. return specifiers_list