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