| #!/usr/bin/env vpython3 |
| # Copyright 2020 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """Script for determining which GPU tests are unexpectedly passing. |
| |
| This script depends on the `bb` tool, which is available as part of depot tools, |
| and the `bq` tool, which is available as part of the Google Cloud SDK |
| https://cloud.google.com/sdk/docs/quickstarts. |
| |
| Example usage: |
| |
| unexpected_pass_finder.py \ |
| --project <BigQuery billing project> \ |
| --suite <test suite to check> \ |
| |
| Concrete example: |
| |
| unexpected_pass_finder.py \ |
| --project luci-resultdb-dev \ |
| --suite pixel |
| |
| You would typically want to pass in --remove-stale-expectations as well in order |
| to have the script automatically remove any expectations it determines are no |
| longer necessary. If a particular expectation proves to be erroneously flagged |
| and removed (e.g. due to a very low flake rate that doesn't get caught |
| consistently by the script), expectations can be omitted from automatic removal |
| using an inline `# finder:disable` comment for a single expectation or a pair of |
| `# finder:disable`/`# finder:enable` comments for a block of expectations. |
| General disables can be handled via `finder:disable-general` and |
| `finder:enable-general`. Disabling removal only if the expectation is found to |
| be unused can be handled via `finder:disable-unused` and `finder:enable-unused`. |
| Disabling removal only if the expectation is found to be stale can be handled |
| via `finder:disable-stale` and `finder:enable-stale`. |
| """ |
| |
| import argparse |
| import datetime |
| import os |
| |
| from gpu_path_util import setup_telemetry_paths # pylint: disable=unused-import |
| from gpu_path_util import setup_testing_paths # pylint: disable=unused-import |
| |
| # Must come after path setup. |
| # pylint: disable=wrong-import-order |
| from unexpected_passes_common import argument_parsing |
| from unexpected_passes_common import builders |
| from unexpected_passes_common import expectations |
| from unexpected_passes_common import result_output |
| # pylint: enable=wrong-import-order |
| |
| from gpu_tests import gpu_integration_test |
| |
| from unexpected_passes import gpu_builders |
| from unexpected_passes import gpu_expectations |
| from unexpected_passes import gpu_queries |
| |
| |
| def ParseArgs() -> argparse.Namespace: |
| name_mapping = gpu_integration_test.GenerateTestNameMapping() |
| test_suites = list(name_mapping.keys()) |
| test_suites.sort() |
| |
| parser = argparse.ArgumentParser( |
| description=('Script for finding cases of stale expectations that can ' |
| 'be removed/modified.')) |
| argument_parsing.AddCommonArguments(parser) |
| |
| input_group = parser.add_mutually_exclusive_group() |
| input_group.add_argument( |
| '--expectation-file', |
| help='A path to an expectation file to read from. If not specified and ' |
| '--test is not used, will automatically determine based off the ' |
| 'provided suite.') |
| input_group.add_argument( |
| '--test', |
| action='append', |
| dest='tests', |
| default=[], |
| help='The name of a test to check for unexpected passes. Can be passed ' |
| 'multiple times to specify multiple tests. Will be treated as if it was ' |
| 'expected to be flaky on all configurations.') |
| parser.add_argument('--suite', |
| required=True, |
| choices=test_suites, |
| help='The test suite being checked.') |
| |
| args = parser.parse_args() |
| argument_parsing.PerformCommonPostParseSetup(args) |
| suite_class = name_mapping[args.suite] |
| |
| if not (args.tests or args.expectation_file): |
| expectation_files = suite_class.ExpectationsFiles() |
| if not expectation_files: |
| raise RuntimeError( |
| f'Suite {args.suite} does not specify an expectation file and is ' |
| f'thus not compatible with this script.') |
| if len(expectation_files) > 1: |
| raise RuntimeError( |
| f'Suite {suite_class} specifies {len(expectation_files)} expectation ' |
| f'files when only 1 is supported.') |
| args.expectation_file = expectation_files[0] |
| |
| if args.remove_stale_expectations and not args.expectation_file: |
| parser.error( |
| '--remove-stale-expectations can only be used with expectation files') |
| |
| # Change to whatever repo the test suite claims the expectation file lives in. |
| # This allows the script to work for most suites if run from outside of |
| # chromium/src. Similarly, it allows suites such as WebGPU CTS that have |
| # expectation files in a different repo to be work when run from chromium/src. |
| os.chdir(suite_class.GetExpectationsFilesRepoPath()) |
| |
| return args |
| |
| |
| # pylint: disable=too-many-locals |
| def main() -> None: |
| args = ParseArgs() |
| |
| builders_instance = gpu_builders.GpuBuilders(args.suite, |
| args.include_internal_builders) |
| builders.RegisterInstance(builders_instance) |
| expectations_instance = gpu_expectations.GpuExpectations() |
| expectations.RegisterInstance(expectations_instance) |
| |
| test_expectation_map = expectations_instance.CreateTestExpectationMap( |
| args.expectation_file, args.tests, |
| datetime.timedelta(days=args.expectation_grace_period)) |
| ci_builders = builders_instance.GetCiBuilders() |
| |
| querier = gpu_queries.GpuBigQueryQuerier(args.suite, args.project, |
| args.num_samples, |
| args.keep_unmatched_results) |
| # Unmatched results are mainly useful for script maintainers, as they don't |
| # provide any additional information for the purposes of finding unexpectedly |
| # passing tests or unused expectations. |
| unmatched = querier.FillExpectationMapForBuilders(test_expectation_map, |
| ci_builders) |
| try_builders = builders_instance.GetTryBuilders(ci_builders) |
| unmatched.update( |
| querier.FillExpectationMapForBuilders(test_expectation_map, try_builders)) |
| unused_expectations = test_expectation_map.FilterOutUnusedExpectations() |
| stale, semi_stale, active = test_expectation_map.SplitByStaleness() |
| if args.result_output_file: |
| with open(args.result_output_file, 'w', encoding='utf-8') as outfile: |
| result_output.OutputResults(stale, semi_stale, active, unmatched, |
| unused_expectations, args.output_format, |
| outfile) |
| else: |
| result_output.OutputResults(stale, semi_stale, active, unmatched, |
| unused_expectations, args.output_format) |
| |
| affected_urls = set() |
| stale_message = '' |
| if args.remove_stale_expectations: |
| for expectation_file, expectation_map in stale.items(): |
| affected_urls |= expectations_instance.RemoveExpectationsFromFile( |
| expectation_map.keys(), expectation_file, |
| expectations.RemovalType.STALE) |
| stale_message += (f'Stale expectations removed from {expectation_file}. ' |
| f'Stale comments, etc. may still need to be removed.\n') |
| for expectation_file, unused_list in unused_expectations.items(): |
| affected_urls |= expectations_instance.RemoveExpectationsFromFile( |
| unused_list, expectation_file, expectations.RemovalType.UNUSED) |
| stale_message += (f'Unused expectations removed from {expectation_file}. ' |
| f'Stale comments, etc. may still need to be removed.\n') |
| |
| if args.narrow_semi_stale_expectation_scope: |
| affected_urls |= expectations_instance.NarrowSemiStaleExpectationScope( |
| semi_stale) |
| stale_message += (f'Semi-stale expectations narrowed in ' |
| f'{args.expectation_file}. Stale comments, etc. may ' |
| f'still need to be removed.\n') |
| |
| if stale_message: |
| print(stale_message) |
| if affected_urls: |
| orphaned_urls = expectations_instance.FindOrphanedBugs(affected_urls) |
| if args.bug_output_file: |
| with open(args.bug_output_file, 'w', encoding='utf-8') as bug_outfile: |
| result_output.OutputAffectedUrls(affected_urls, |
| orphaned_urls, |
| bug_outfile, |
| auto_close_bugs=args.auto_close_bugs) |
| else: |
| result_output.OutputAffectedUrls(affected_urls, |
| orphaned_urls, |
| auto_close_bugs=args.auto_close_bugs) |
| # pylint: enable=too-many-locals |
| |
| |
| if __name__ == '__main__': |
| main() |