| #!/usr/bin/env python3 |
| |
| # Copyright 2020 The gRPC Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| # Library to extract scenario definitions from scenario_config.py. |
| # |
| # Contains functions to filter, analyze and dump scenario definitions. |
| # |
| # This library is used in loadtest_config.py to generate the "scenariosJSON" |
| # field in the format accepted by the OSS benchmarks framework. |
| # See https://github.com/grpc/test-infra/blob/master/config/samples/cxx_example_loadtest.yaml |
| # |
| # It can also be used to dump scenarios to files, to count scenarios by |
| # language, and to export scenario languages in a format that can be used for |
| # automation. |
| # |
| # Example usage: |
| # |
| # scenario_config.py --export_scenarios -l cxx -f cxx_scenario_ -r '.*' \ |
| # --category=scalable |
| # |
| # scenario_config.py --count_scenarios |
| # |
| # scenario_config.py --count_scenarios --category=scalable |
| # |
| # For usage of the language config output, see loadtest_config.py. |
| |
| import argparse |
| import collections |
| import json |
| import re |
| import sys |
| from typing import Any, Callable, Dict, Iterable, NamedTuple |
| |
| import scenario_config |
| |
| # Language parameters for load test config generation. |
| |
| LanguageConfig = NamedTuple( |
| "LanguageConfig", |
| [ |
| ("category", str), |
| ("language", str), |
| ("client_language", str), |
| ("server_language", str), |
| ], |
| ) |
| |
| |
| def category_string(categories: Iterable[str], category: str) -> str: |
| """Converts a list of categories into a single string for counting.""" |
| if category != "all": |
| return category if category in categories else "" |
| |
| main_categories = ("scalable", "smoketest") |
| s = set(categories) |
| |
| c = [m for m in main_categories if m in s] |
| s.difference_update(main_categories) |
| c.extend(s) |
| return " ".join(c) |
| |
| |
| def gen_scenario_languages(category: str) -> Iterable[LanguageConfig]: |
| """Generates tuples containing the languages specified in each scenario.""" |
| for language in scenario_config.LANGUAGES: |
| for scenario in scenario_config.LANGUAGES[language].scenarios(): |
| client_language = scenario.get("CLIENT_LANGUAGE", "") |
| server_language = scenario.get("SERVER_LANGUAGE", "") |
| categories = scenario.get("CATEGORIES", []) |
| if category != "all" and category not in categories: |
| continue |
| cat = category_string(categories, category) |
| yield LanguageConfig( |
| category=cat, |
| language=language, |
| client_language=client_language, |
| server_language=server_language, |
| ) |
| |
| |
| def scenario_filter( |
| scenario_name_regex: str = ".*", |
| category: str = "all", |
| client_language: str = "", |
| server_language: str = "", |
| ) -> Callable[[Dict[str, Any]], bool]: |
| """Returns a function to filter scenarios to process.""" |
| |
| def filter_scenario(scenario: Dict[str, Any]) -> bool: |
| """Filters scenarios that match specified criteria.""" |
| if not re.search(scenario_name_regex, scenario["name"]): |
| return False |
| # if the 'CATEGORIES' key is missing, treat scenario as part of |
| # 'scalable' and 'smoketest'. This matches the behavior of |
| # run_performance_tests.py. |
| scenario_categories = scenario.get( |
| "CATEGORIES", ["scalable", "smoketest"] |
| ) |
| if category not in scenario_categories and category != "all": |
| return False |
| |
| scenario_client_language = scenario.get("CLIENT_LANGUAGE", "") |
| if client_language != scenario_client_language: |
| return False |
| |
| scenario_server_language = scenario.get("SERVER_LANGUAGE", "") |
| if server_language != scenario_server_language: |
| return False |
| |
| return True |
| |
| return filter_scenario |
| |
| |
| def gen_scenarios( |
| language_name: str, |
| scenario_filter_function: Callable[[Dict[str, Any]], bool], |
| ) -> Iterable[Dict[str, Any]]: |
| """Generates scenarios that match a given filter function.""" |
| return map( |
| scenario_config.remove_nonproto_fields, |
| filter( |
| scenario_filter_function, |
| scenario_config.LANGUAGES[language_name].scenarios(), |
| ), |
| ) |
| |
| |
| def dump_to_json_files( |
| scenarios: Iterable[Dict[str, Any]], filename_prefix: str |
| ) -> None: |
| """Dumps a list of scenarios to JSON files""" |
| count = 0 |
| for scenario in scenarios: |
| filename = "{}{}.json".format(filename_prefix, scenario["name"]) |
| print("Writing file {}".format(filename), file=sys.stderr) |
| with open(filename, "w") as outfile: |
| # The dump file should have {"scenarios" : []} as the top level |
| # element, when embedded in a LoadTest configuration YAML file. |
| json.dump({"scenarios": [scenario]}, outfile, indent=2) |
| count += 1 |
| print("Wrote {} scenarios".format(count), file=sys.stderr) |
| |
| |
| def main() -> None: |
| language_choices = sorted(scenario_config.LANGUAGES.keys()) |
| argp = argparse.ArgumentParser(description="Exports scenarios to files.") |
| argp.add_argument( |
| "--export_scenarios", |
| action="store_true", |
| help="Export scenarios to JSON files.", |
| ) |
| argp.add_argument( |
| "--count_scenarios", |
| action="store_true", |
| help="Count scenarios for all test languages.", |
| ) |
| argp.add_argument( |
| "-l", "--language", choices=language_choices, help="Language to export." |
| ) |
| argp.add_argument( |
| "-f", |
| "--filename_prefix", |
| default="scenario_dump_", |
| type=str, |
| help="Prefix for exported JSON file names.", |
| ) |
| argp.add_argument( |
| "-r", |
| "--regex", |
| default=".*", |
| type=str, |
| help="Regex to select scenarios to run.", |
| ) |
| argp.add_argument( |
| "--category", |
| default="all", |
| choices=[ |
| "all", |
| "inproc", |
| "scalable", |
| "smoketest", |
| "sweep", |
| "psm", |
| "dashboard", |
| ], |
| help="Select scenarios for a category of tests.", |
| ) |
| argp.add_argument( |
| "--client_language", |
| default="", |
| choices=language_choices, |
| help="Select only scenarios with a specified client language.", |
| ) |
| argp.add_argument( |
| "--server_language", |
| default="", |
| choices=language_choices, |
| help="Select only scenarios with a specified server language.", |
| ) |
| args = argp.parse_args() |
| |
| if args.export_scenarios and not args.language: |
| print( |
| "Dumping scenarios requires a specified language.", file=sys.stderr |
| ) |
| argp.print_usage(file=sys.stderr) |
| return |
| |
| if args.export_scenarios: |
| s_filter = scenario_filter( |
| scenario_name_regex=args.regex, |
| category=args.category, |
| client_language=args.client_language, |
| server_language=args.server_language, |
| ) |
| scenarios = gen_scenarios(args.language, s_filter) |
| dump_to_json_files(scenarios, args.filename_prefix) |
| |
| if args.count_scenarios: |
| print( |
| "Scenario count for all languages (category: {}):".format( |
| args.category |
| ) |
| ) |
| print( |
| "{:>5} {:16} {:8} {:8} {}".format( |
| "Count", "Language", "Client", "Server", "Categories" |
| ) |
| ) |
| c = collections.Counter(gen_scenario_languages(args.category)) |
| total = 0 |
| for (cat, l, cl, sl), count in c.most_common(): |
| print( |
| "{count:5} {l:16} {cl:8} {sl:8} {cat}".format( |
| l=l, cl=cl, sl=sl, count=count, cat=cat |
| ) |
| ) |
| total += count |
| |
| print( |
| "\n{:>5} total scenarios (category: {})".format( |
| total, args.category |
| ) |
| ) |
| |
| |
| if __name__ == "__main__": |
| main() |