data_types.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. # Copyright 2020 The Chromium Authors. All rights reserved.
  2. # Use of this source code is governed by a BSD-style license that can be
  3. # found in the LICENSE file.
  4. """Various custom data types for use throughout the unexpected pass finder."""
  5. from __future__ import print_function
  6. import collections
  7. import copy
  8. import fnmatch
  9. import logging
  10. from typing import (Any, Dict, Generator, Iterable, List, Optional, Set, Tuple,
  11. Type, Union)
  12. import six
  13. FULL_PASS = 1
  14. NEVER_PASS = 2
  15. PARTIAL_PASS = 3
  16. # Allow different unexpected pass finder implementations to register custom
  17. # data types if necessary. These are set to the base versions at the end of the
  18. # file.
  19. Expectation = None
  20. Result = None
  21. BuildStats = None
  22. TestExpectationMap = None
  23. # Type hinting aliases.
  24. ResultListType = List['BaseResult']
  25. ResultSetType = Set['BaseResult']
  26. def SetExpectationImplementation(impl: Type['BaseExpectation']) -> None:
  27. global Expectation
  28. assert issubclass(impl, BaseExpectation)
  29. Expectation = impl
  30. def SetResultImplementation(impl: Type['BaseResult']) -> None:
  31. global Result
  32. assert issubclass(impl, BaseResult)
  33. Result = impl
  34. def SetBuildStatsImplementation(impl: Type['BaseBuildStats']) -> None:
  35. global BuildStats
  36. assert issubclass(impl, BaseBuildStats)
  37. BuildStats = impl
  38. def SetTestExpectationMapImplementation(impl: Type['BaseTestExpectationMap']
  39. ) -> None:
  40. global TestExpectationMap
  41. assert issubclass(impl, BaseTestExpectationMap)
  42. TestExpectationMap = impl
  43. class BaseExpectation():
  44. """Container for a test expectation.
  45. Similar to typ's expectations_parser.Expectation class, but with unnecessary
  46. data stripped out and made hashable.
  47. The data contained in an Expectation is equivalent to a single line in an
  48. expectation file.
  49. """
  50. def __init__(self,
  51. test: str,
  52. tags: Iterable[str],
  53. expected_results: Union[str, Iterable[str]],
  54. bug: Optional[str] = None):
  55. self.test = test
  56. self.tags = frozenset(tags)
  57. self.bug = bug or ''
  58. if isinstance(expected_results, str):
  59. self.expected_results = frozenset([expected_results])
  60. else:
  61. self.expected_results = frozenset(expected_results)
  62. # We're going to be making a lot of comparisons, and fnmatch is *much*
  63. # slower (~40x from rough testing) than a straight comparison, so only use
  64. # it if necessary.
  65. if '*' in test:
  66. self._comp = self._CompareWildcard
  67. else:
  68. self._comp = self._CompareNonWildcard
  69. def __eq__(self, other: Any) -> bool:
  70. return (isinstance(other, BaseExpectation) and self.test == other.test
  71. and self.tags == other.tags
  72. and self.expected_results == other.expected_results
  73. and self.bug == other.bug)
  74. def __ne__(self, other: Any) -> bool:
  75. return not self.__eq__(other)
  76. def __hash__(self) -> int:
  77. return hash((self.test, self.tags, self.expected_results, self.bug))
  78. def _CompareWildcard(self, result_test_name: str) -> bool:
  79. return fnmatch.fnmatch(result_test_name, self.test)
  80. def _CompareNonWildcard(self, result_test_name: str) -> bool:
  81. return result_test_name == self.test
  82. def AppliesToResult(self, result: 'BaseResult') -> bool:
  83. """Checks whether this expectation should have applied to |result|.
  84. An expectation applies to a result if the test names match (including
  85. wildcard expansion) and the expectation's tags are a subset of the result's
  86. tags.
  87. Args:
  88. result: A Result instance to check against.
  89. Returns:
  90. True if |self| applies to |result|, otherwise False.
  91. """
  92. assert isinstance(result, BaseResult)
  93. return (self._comp(result.test) and self.tags <= result.tags)
  94. def MaybeAppliesToTest(self, test_name: str) -> bool:
  95. """Similar to AppliesToResult, but used to do initial filtering.
  96. Args:
  97. test_name: A string containing the name of a test.
  98. Returns:
  99. True if |self| could apply to a test named |test_name|, otherwise False.
  100. """
  101. return self._comp(test_name)
  102. class BaseResult():
  103. """Container for a test result.
  104. Contains the minimal amount of data necessary to describe/identify a result
  105. from ResultDB for the purposes of the unexpected pass finder.
  106. """
  107. def __init__(self, test: str, tags: Iterable[str], actual_result: str,
  108. step: str, build_id: str):
  109. """
  110. Args:
  111. test: A string containing the name of the test.
  112. tags: An iterable containing the typ tags for the result.
  113. actual_result: The actual result of the test as a string.
  114. step: A string containing the name of the step on the builder.
  115. build_id: A string containing the Buildbucket ID for the build this result
  116. came from.
  117. """
  118. self.test = test
  119. self.tags = frozenset(tags)
  120. self.actual_result = actual_result
  121. self.step = step
  122. self.build_id = build_id
  123. def __eq__(self, other: Any) -> bool:
  124. return (isinstance(other, BaseResult) and self.test == other.test
  125. and self.tags == other.tags
  126. and self.actual_result == other.actual_result
  127. and self.step == other.step and self.build_id == other.build_id)
  128. def __ne__(self, other: Any) -> bool:
  129. return not self.__eq__(other)
  130. def __hash__(self) -> int:
  131. return hash(
  132. (self.test, self.tags, self.actual_result, self.step, self.build_id))
  133. class BaseBuildStats():
  134. """Container for keeping track of a builder's pass/fail stats."""
  135. def __init__(self):
  136. self.passed_builds = 0
  137. self.total_builds = 0
  138. self.failure_links = frozenset()
  139. @property
  140. def failed_builds(self) -> int:
  141. return self.total_builds - self.passed_builds
  142. @property
  143. def did_fully_pass(self) -> bool:
  144. return self.passed_builds == self.total_builds
  145. @property
  146. def did_never_pass(self) -> bool:
  147. return self.failed_builds == self.total_builds
  148. def AddPassedBuild(self) -> None:
  149. self.passed_builds += 1
  150. self.total_builds += 1
  151. def AddFailedBuild(self, build_id: str) -> None:
  152. self.total_builds += 1
  153. build_link = BuildLinkFromBuildId(build_id)
  154. self.failure_links = frozenset([build_link]) | self.failure_links
  155. def GetStatsAsString(self) -> str:
  156. return '(%d/%d passed)' % (self.passed_builds, self.total_builds)
  157. # pylint:disable=unused-argument
  158. def NeverNeededExpectation(self, expectation: BaseExpectation) -> bool:
  159. """Returns whether the results tallied in |self| never needed |expectation|.
  160. Args:
  161. expectation: An Expectation object that |stats| is located under.
  162. Returns:
  163. True if all the results tallied in |self| would have passed without
  164. |expectation| being present. Otherwise, False.
  165. """
  166. return self.did_fully_pass
  167. # pylint:enable=unused-argument
  168. # pylint:disable=unused-argument
  169. def AlwaysNeededExpectation(self, expectation: BaseExpectation) -> bool:
  170. """Returns whether the results tallied in |self| always needed |expectation.
  171. Args:
  172. expectation: An Expectation object that |stats| is located under.
  173. Returns:
  174. True if all the results tallied in |self| would have failed without
  175. |expectation| being present. Otherwise, False.
  176. """
  177. return self.did_never_pass
  178. # pylint:enable=unused-argument
  179. def __eq__(self, other: Any) -> bool:
  180. return (isinstance(other, BuildStats)
  181. and self.passed_builds == other.passed_builds
  182. and self.total_builds == other.total_builds
  183. and self.failure_links == other.failure_links)
  184. def __ne__(self, other: Any) -> bool:
  185. return not self.__eq__(other)
  186. def BuildLinkFromBuildId(build_id: str) -> str:
  187. return 'http://ci.chromium.org/b/%s' % build_id
  188. # These explicit overrides could likely be replaced by using regular dicts with
  189. # type hinting in Python 3. Based on https://stackoverflow.com/a/2588648, this
  190. # should cover all cases where the dict can be modified.
  191. class BaseTypedMap(dict):
  192. """A base class for typed dictionaries.
  193. Any child classes that override __setitem__ will have any modifications to the
  194. dictionary go through the type checking in __setitem__.
  195. """
  196. def __init__(self, *args, **kwargs): # pylint:disable=super-init-not-called
  197. self.update(*args, **kwargs)
  198. def update(self, *args, **kwargs) -> None:
  199. if args:
  200. assert len(args) == 1
  201. other = dict(args[0])
  202. for k, v in other.items():
  203. self[k] = v
  204. for k, v in kwargs.items():
  205. self[k] = v
  206. def setdefault(self, key: Any, value: Any = None) -> Any:
  207. if key not in self:
  208. self[key] = value
  209. return self[key]
  210. def _value_type(self) -> type:
  211. raise NotImplementedError()
  212. def IterToValueType(self, value_type: type) -> Generator[tuple, None, None]:
  213. """Recursively iterates over contents until |value_type| is found.
  214. Used to get rid of nested loops, instead using a single loop that
  215. automatically iterates through all the contents at a certain depth.
  216. Args:
  217. value_type: The type to recurse to and then iterate over. For example,
  218. "BuilderStepMap" would result in iterating over the BuilderStepMap
  219. values, meaning that the returned generator would create tuples in the
  220. form (test_name, expectation, builder_map).
  221. Returns:
  222. A generator that yields tuples. The length and content of the tuples will
  223. vary depending on |value_type|. For example, using "BuilderStepMap" would
  224. result in tuples of the form (test_name, expectation, builder_map), while
  225. "BuildStats" would result in (test_name, expectation, builder_name,
  226. step_name, build_stats).
  227. """
  228. if self._value_type() == value_type:
  229. for k, v in self.items():
  230. yield k, v
  231. else:
  232. for k, v in self.items():
  233. for nested_value in v.IterToValueType(value_type):
  234. yield (k, ) + nested_value
  235. def Merge(self,
  236. other_map: 'BaseTypedMap',
  237. reference_map: Optional[dict] = None) -> None:
  238. """Merges |other_map| into self.
  239. Args:
  240. other_map: A BaseTypedMap whose contents will be merged into self.
  241. reference_map: A dict containing the information that was originally in
  242. self. Used for ensuring that a single expectation/builder/step
  243. combination is only ever updated once. If None, a copy of self will be
  244. used.
  245. """
  246. assert isinstance(other_map, self.__class__)
  247. # We should only ever encounter a single updated BuildStats for an
  248. # expectation/builder/step combination. Use the reference map to determine
  249. # if a particular BuildStats has already been updated or not.
  250. reference_map = reference_map or copy.deepcopy(self)
  251. for key, value in other_map.items():
  252. if key not in self:
  253. self[key] = value
  254. else:
  255. if isinstance(value, dict):
  256. self[key].Merge(value, reference_map.get(key, {}))
  257. else:
  258. assert isinstance(value, BuildStats)
  259. # Ensure we haven't updated this BuildStats already. If the reference
  260. # map doesn't have a corresponding BuildStats, then base_map shouldn't
  261. # have initially either, and thus it would have been added before
  262. # reaching this point. Otherwise, the two values must match, meaning
  263. # that base_map's BuildStats hasn't been updated yet.
  264. reference_stats = reference_map.get(key, None)
  265. assert reference_stats is not None
  266. assert reference_stats == self[key]
  267. self[key] = value
  268. class BaseTestExpectationMap(BaseTypedMap):
  269. """Typed map for string types -> ExpectationBuilderMap.
  270. This results in a dict in the following format:
  271. {
  272. expectation_file1 (str): {
  273. expectation1 (data_types.Expectation): {
  274. builder_name1 (str): {
  275. step_name1 (str): stats1 (data_types.BuildStats),
  276. step_name2 (str): stats2 (data_types.BuildStats),
  277. ...
  278. },
  279. builder_name2 (str): { ... },
  280. },
  281. expectation2 (data_types.Expectation): { ... },
  282. ...
  283. },
  284. expectation_file2 (str): { ... },
  285. ...
  286. }
  287. """
  288. def __setitem__(self, key: str, value: 'ExpectationBuilderMap') -> None:
  289. assert IsStringType(key)
  290. assert isinstance(value, ExpectationBuilderMap)
  291. super().__setitem__(key, value)
  292. def _value_type(self) -> type:
  293. return ExpectationBuilderMap
  294. def IterBuilderStepMaps(
  295. self
  296. ) -> Generator[Tuple[str, BaseExpectation, 'BuilderStepMap'], None, None]:
  297. """Iterates over all BuilderStepMaps contained in the map.
  298. Returns:
  299. A generator yielding tuples in the form (expectation_file (str),
  300. expectation (Expectation), builder_map (BuilderStepMap))
  301. """
  302. return self.IterToValueType(BuilderStepMap)
  303. def AddResultList(self,
  304. builder: str,
  305. results: ResultListType,
  306. expectation_files: Optional[Iterable[str]] = None
  307. ) -> ResultListType:
  308. """Adds |results| to |self|.
  309. Args:
  310. builder: A string containing the builder |results| came from. Should be
  311. prefixed with something to distinguish between identically named CI
  312. and try builders.
  313. results: A list of data_types.Result objects corresponding to the ResultDB
  314. data queried for |builder|.
  315. expectation_files: An iterable of expectation file names that these
  316. results could possibly apply to. If None, then expectations from all
  317. known expectation files will be used.
  318. Returns:
  319. A list of data_types.Result objects who did not have a matching
  320. expectation in |self|.
  321. """
  322. failure_results = set()
  323. pass_results = set()
  324. unmatched_results = []
  325. for r in results:
  326. if r.actual_result == 'Pass':
  327. pass_results.add(r)
  328. else:
  329. failure_results.add(r)
  330. # Remove any cases of failure -> pass from the passing set. If a test is
  331. # flaky, we get both pass and failure results for it, so we need to remove
  332. # the any cases of a pass result having a corresponding, earlier failure
  333. # result.
  334. modified_failing_retry_results = set()
  335. for r in failure_results:
  336. modified_failing_retry_results.add(
  337. Result(r.test, r.tags, 'Pass', r.step, r.build_id))
  338. pass_results -= modified_failing_retry_results
  339. # Group identically named results together so we reduce the number of
  340. # comparisons we have to make.
  341. all_results = pass_results | failure_results
  342. grouped_results = collections.defaultdict(list)
  343. for r in all_results:
  344. grouped_results[r.test].append(r)
  345. matched_results = self._AddGroupedResults(grouped_results, builder,
  346. expectation_files)
  347. unmatched_results = list(all_results - matched_results)
  348. return unmatched_results
  349. def _AddGroupedResults(self, grouped_results: Dict[str, ResultListType],
  350. builder: str, expectation_files: Optional[List[str]]
  351. ) -> ResultSetType:
  352. """Adds all results in |grouped_results| to |self|.
  353. Args:
  354. grouped_results: A dict mapping test name (str) to a list of
  355. data_types.Result objects for that test.
  356. builder: A string containing the name of the builder |grouped_results|
  357. came from.
  358. expectation_files: An iterable of expectation file names that these
  359. results could possibly apply to. If None, then expectations from all
  360. known expectation files will be used.
  361. Returns:
  362. A set of data_types.Result objects that had at least one matching
  363. expectation.
  364. """
  365. matched_results = set()
  366. for test_name, result_list in grouped_results.items():
  367. for ef, expectation_map in self.items():
  368. if expectation_files is not None and ef not in expectation_files:
  369. continue
  370. for expectation, builder_map in expectation_map.items():
  371. if not expectation.MaybeAppliesToTest(test_name):
  372. continue
  373. for r in result_list:
  374. if expectation.AppliesToResult(r):
  375. matched_results.add(r)
  376. step_map = builder_map.setdefault(builder, StepBuildStatsMap())
  377. stats = step_map.setdefault(r.step, BuildStats())
  378. self._AddSingleResult(r, stats)
  379. return matched_results
  380. def _AddSingleResult(self, result: BaseResult, stats: BaseBuildStats) -> None:
  381. """Adds |result| to |self|.
  382. Args:
  383. result: A data_types.Result object to add.
  384. stats: A data_types.BuildStats object to add the result to.
  385. """
  386. if result.actual_result == 'Pass':
  387. stats.AddPassedBuild()
  388. else:
  389. stats.AddFailedBuild(result.build_id)
  390. def SplitByStaleness(
  391. self) -> Tuple['BaseTestExpectationMap', 'BaseTestExpectationMap',
  392. 'BaseTestExpectationMap']:
  393. """Separates stored data based on expectation staleness.
  394. Returns:
  395. Three TestExpectationMaps (stale_dict, semi_stale_dict, active_dict). All
  396. three combined contain the information of |self|. |stale_dict| contains
  397. entries for expectations that are no longer being helpful,
  398. |semi_stale_dict| contains entries for expectations that might be
  399. removable or modifiable, but have at least one failed test run.
  400. |active_dict| contains entries for expectations that are preventing
  401. failures on all builders they're active on, and thus shouldn't be removed.
  402. """
  403. stale_dict = TestExpectationMap()
  404. semi_stale_dict = TestExpectationMap()
  405. active_dict = TestExpectationMap()
  406. # This initially looks like a good target for using
  407. # TestExpectationMap's iterators since there are many nested loops.
  408. # However, we need to reset state in different loops, and the alternative of
  409. # keeping all the state outside the loop and resetting under certain
  410. # conditions ends up being less readable than just using nested loops.
  411. for expectation_file, expectation_map in self.items():
  412. for expectation, builder_map in expectation_map.items():
  413. # A temporary map to hold data so we can later determine whether an
  414. # expectation is stale, semi-stale, or active.
  415. tmp_map = {
  416. FULL_PASS: BuilderStepMap(),
  417. NEVER_PASS: BuilderStepMap(),
  418. PARTIAL_PASS: BuilderStepMap(),
  419. }
  420. split_stats_map = builder_map.SplitBuildStatsByPass(expectation)
  421. for builder_name, (fully_passed, never_passed,
  422. partially_passed) in split_stats_map.items():
  423. if fully_passed:
  424. tmp_map[FULL_PASS][builder_name] = fully_passed
  425. if never_passed:
  426. tmp_map[NEVER_PASS][builder_name] = never_passed
  427. if partially_passed:
  428. tmp_map[PARTIAL_PASS][builder_name] = partially_passed
  429. def _CopyPassesIntoBuilderMap(builder_map, pass_types):
  430. for pt in pass_types:
  431. for builder, steps in tmp_map[pt].items():
  432. builder_map.setdefault(builder, StepBuildStatsMap()).update(steps)
  433. # Handle the case of a stale expectation.
  434. if not (tmp_map[NEVER_PASS] or tmp_map[PARTIAL_PASS]):
  435. builder_map = stale_dict.setdefault(
  436. expectation_file,
  437. ExpectationBuilderMap()).setdefault(expectation, BuilderStepMap())
  438. _CopyPassesIntoBuilderMap(builder_map, [FULL_PASS])
  439. # Handle the case of an active expectation.
  440. elif not tmp_map[FULL_PASS]:
  441. builder_map = active_dict.setdefault(
  442. expectation_file,
  443. ExpectationBuilderMap()).setdefault(expectation, BuilderStepMap())
  444. _CopyPassesIntoBuilderMap(builder_map, [NEVER_PASS, PARTIAL_PASS])
  445. # Handle the case of a semi-stale expectation that should be considered
  446. # active.
  447. elif self._ShouldTreatSemiStaleAsActive(tmp_map):
  448. builder_map = active_dict.setdefault(
  449. expectation_file,
  450. ExpectationBuilderMap()).setdefault(expectation, BuilderStepMap())
  451. _CopyPassesIntoBuilderMap(builder_map,
  452. [FULL_PASS, PARTIAL_PASS, NEVER_PASS])
  453. # Handle the case of a semi-stale expectation.
  454. else:
  455. # TODO(crbug.com/998329): Sort by pass percentage so it's easier to
  456. # find problematic builders without highlighting.
  457. builder_map = semi_stale_dict.setdefault(
  458. expectation_file,
  459. ExpectationBuilderMap()).setdefault(expectation, BuilderStepMap())
  460. _CopyPassesIntoBuilderMap(builder_map,
  461. [FULL_PASS, PARTIAL_PASS, NEVER_PASS])
  462. return stale_dict, semi_stale_dict, active_dict
  463. def _ShouldTreatSemiStaleAsActive(self, pass_map: Dict[int, 'BuilderStepMap']
  464. ) -> bool:
  465. """Check if a semi-stale expectation should be treated as active.
  466. Allows for implementation-specific workarounds.
  467. Args:
  468. pass_map: A dict mapping the FULL/NEVER/PARTIAL_PASS constants to
  469. BuilderStepMaps, as used in self.SplitByStaleness().
  470. Returns:
  471. A boolean denoting whether the given results data should be treated as an
  472. active expectation instead of a semi-stale one.
  473. """
  474. del pass_map
  475. return False
  476. def FilterOutUnusedExpectations(self) -> Dict[str, List[BaseExpectation]]:
  477. """Filters out any unused Expectations from stored data.
  478. An Expectation is considered unused if its corresponding dictionary is
  479. empty. If removing Expectations results in a top-level test key having an
  480. empty dictionary, that test entry will also be removed.
  481. Returns:
  482. A dict from expectation file name (str) to list of unused
  483. data_types.Expectation from that file.
  484. """
  485. logging.info('Filtering out unused expectations')
  486. unused = collections.defaultdict(list)
  487. unused_count = 0
  488. for (expectation_file, expectation,
  489. builder_map) in self.IterBuilderStepMaps():
  490. if not builder_map:
  491. unused[expectation_file].append(expectation)
  492. unused_count += 1
  493. for expectation_file, expectations in unused.items():
  494. for e in expectations:
  495. del self[expectation_file][e]
  496. logging.debug('Found %d unused expectations', unused_count)
  497. empty_files = []
  498. for expectation_file, expectation_map in self.items():
  499. if not expectation_map:
  500. empty_files.append(expectation_file)
  501. for empty in empty_files:
  502. del self[empty]
  503. logging.debug('Found %d empty files: %s', len(empty_files), empty_files)
  504. return unused
  505. class ExpectationBuilderMap(BaseTypedMap):
  506. """Typed map for Expectation -> BuilderStepMap."""
  507. def __setitem__(self, key: BaseExpectation, value: 'BuilderStepMap') -> None:
  508. assert isinstance(key, BaseExpectation)
  509. assert isinstance(value, self._value_type())
  510. super().__setitem__(key, value)
  511. def _value_type(self) -> type:
  512. return BuilderStepMap
  513. class BuilderStepMap(BaseTypedMap):
  514. """Typed map for string types -> StepBuildStatsMap."""
  515. def __setitem__(self, key: str, value: 'StepBuildStatsMap') -> None:
  516. assert IsStringType(key)
  517. assert isinstance(value, self._value_type())
  518. super().__setitem__(key, value)
  519. def _value_type(self) -> type:
  520. return StepBuildStatsMap
  521. def SplitBuildStatsByPass(
  522. self, expectation: BaseExpectation
  523. ) -> Dict[str, Tuple['StepBuildStatsMap', 'StepBuildStatsMap',
  524. 'StepBuildStatsMap']]:
  525. """Splits the underlying BuildStats data by passing-ness.
  526. Args:
  527. expectation: The Expectation that this BuilderStepMap is located under.
  528. Returns:
  529. A dict mapping builder name to a tuple (fully_passed, never_passed,
  530. partially_passed). Each *_passed is a StepBuildStatsMap containing data
  531. for the steps that either fully passed on all builds, never passed on any
  532. builds, or passed some of the time.
  533. """
  534. retval = {}
  535. for builder_name, step_map in self.items():
  536. fully_passed = StepBuildStatsMap()
  537. never_passed = StepBuildStatsMap()
  538. partially_passed = StepBuildStatsMap()
  539. for step_name, stats in step_map.items():
  540. if stats.NeverNeededExpectation(expectation):
  541. assert step_name not in fully_passed
  542. fully_passed[step_name] = stats
  543. elif stats.AlwaysNeededExpectation(expectation):
  544. assert step_name not in never_passed
  545. never_passed[step_name] = stats
  546. else:
  547. assert step_name not in partially_passed
  548. partially_passed[step_name] = stats
  549. retval[builder_name] = (fully_passed, never_passed, partially_passed)
  550. return retval
  551. def IterBuildStats(
  552. self) -> Generator[Tuple[str, str, BaseBuildStats], None, None]:
  553. """Iterates over all BuildStats contained in the map.
  554. Returns:
  555. A generator yielding tuples in the form (builder_name (str), step_name
  556. (str), build_stats (BuildStats)).
  557. """
  558. return self.IterToValueType(BuildStats)
  559. class StepBuildStatsMap(BaseTypedMap):
  560. """Typed map for string types -> BuildStats"""
  561. def __setitem__(self, key: str, value: BuildStats) -> None:
  562. assert IsStringType(key)
  563. assert isinstance(value, self._value_type())
  564. super().__setitem__(key, value)
  565. def _value_type(self) -> type:
  566. return BuildStats
  567. class BuilderEntry():
  568. """Simple container for defining a builder."""
  569. def __init__(self, name: str, builder_type: str, is_internal_builder: bool):
  570. """
  571. Args:
  572. name: A string containing the name of the builder.
  573. builder_type: A string containing the type of builder this is, either
  574. "ci" or "try".
  575. is_internal_builder: A boolean denoting whether the builder is internal or
  576. not.
  577. """
  578. self.name = name
  579. self.builder_type = builder_type
  580. self.is_internal_builder = is_internal_builder
  581. @property
  582. def project(self) -> str:
  583. return 'chrome' if self.is_internal_builder else 'chromium'
  584. def __eq__(self, other: Any) -> bool:
  585. return (isinstance(other, BuilderEntry) and self.name == other.name
  586. and self.builder_type == other.builder_type
  587. and self.is_internal_builder == other.is_internal_builder)
  588. def __ne__(self, other: Any) -> bool:
  589. return not self.__eq__(other)
  590. def __hash__(self) -> int:
  591. return hash((self.name, self.builder_type, self.is_internal_builder))
  592. def IsStringType(s: Any) -> bool:
  593. return isinstance(s, six.string_types)
  594. Expectation = BaseExpectation
  595. Result = BaseResult
  596. BuildStats = BaseBuildStats
  597. TestExpectationMap = BaseTestExpectationMap