1#!/usr/bin/env python3 2 3# Copyright 2020 The gRPC Authors 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17# Library to extract scenario definitions from scenario_config.py. 18# 19# Contains functions to filter, analyze and dump scenario definitions. 20# 21# This library is used in loadtest_config.py to generate the "scenariosJSON" 22# field in the format accepted by the OSS benchmarks framework. 23# See https://github.com/grpc/test-infra/blob/master/config/samples/cxx_example_loadtest.yaml 24# 25# It can also be used to dump scenarios to files, to count scenarios by 26# language, and to export scenario languages in a format that can be used for 27# automation. 28# 29# Example usage: 30# 31# scenario_config.py --export_scenarios -l cxx -f cxx_scenario_ -r '.*' \ 32# --category=scalable 33# 34# scenario_config.py --count_scenarios 35# 36# scenario_config.py --count_scenarios --category=scalable 37# 38# For usage of the language config output, see loadtest_config.py. 39 40import argparse 41import collections 42import json 43import re 44import sys 45from typing import Any, Callable, Dict, Iterable, NamedTuple 46 47import scenario_config 48 49# Language parameters for load test config generation. 50 51LanguageConfig = NamedTuple( 52 "LanguageConfig", 53 [ 54 ("category", str), 55 ("language", str), 56 ("client_language", str), 57 ("server_language", str), 58 ], 59) 60 61 62def category_string(categories: Iterable[str], category: str) -> str: 63 """Converts a list of categories into a single string for counting.""" 64 if category != "all": 65 return category if category in categories else "" 66 67 main_categories = ("scalable", "smoketest") 68 s = set(categories) 69 70 c = [m for m in main_categories if m in s] 71 s.difference_update(main_categories) 72 c.extend(s) 73 return " ".join(c) 74 75 76def gen_scenario_languages(category: str) -> Iterable[LanguageConfig]: 77 """Generates tuples containing the languages specified in each scenario.""" 78 for language in scenario_config.LANGUAGES: 79 for scenario in scenario_config.LANGUAGES[language].scenarios(): 80 client_language = scenario.get("CLIENT_LANGUAGE", "") 81 server_language = scenario.get("SERVER_LANGUAGE", "") 82 categories = scenario.get("CATEGORIES", []) 83 if category != "all" and category not in categories: 84 continue 85 cat = category_string(categories, category) 86 yield LanguageConfig( 87 category=cat, 88 language=language, 89 client_language=client_language, 90 server_language=server_language, 91 ) 92 93 94def scenario_filter( 95 scenario_name_regex: str = ".*", 96 category: str = "all", 97 client_language: str = "", 98 server_language: str = "", 99) -> Callable[[Dict[str, Any]], bool]: 100 """Returns a function to filter scenarios to process.""" 101 102 def filter_scenario(scenario: Dict[str, Any]) -> bool: 103 """Filters scenarios that match specified criteria.""" 104 if not re.search(scenario_name_regex, scenario["name"]): 105 return False 106 # if the 'CATEGORIES' key is missing, treat scenario as part of 107 # 'scalable' and 'smoketest'. This matches the behavior of 108 # run_performance_tests.py. 109 scenario_categories = scenario.get( 110 "CATEGORIES", ["scalable", "smoketest"] 111 ) 112 if category not in scenario_categories and category != "all": 113 return False 114 115 scenario_client_language = scenario.get("CLIENT_LANGUAGE", "") 116 if client_language != scenario_client_language: 117 return False 118 119 scenario_server_language = scenario.get("SERVER_LANGUAGE", "") 120 if server_language != scenario_server_language: 121 return False 122 123 return True 124 125 return filter_scenario 126 127 128def gen_scenarios( 129 language_name: str, 130 scenario_filter_function: Callable[[Dict[str, Any]], bool], 131) -> Iterable[Dict[str, Any]]: 132 """Generates scenarios that match a given filter function.""" 133 return map( 134 scenario_config.remove_nonproto_fields, 135 filter( 136 scenario_filter_function, 137 scenario_config.LANGUAGES[language_name].scenarios(), 138 ), 139 ) 140 141 142def dump_to_json_files( 143 scenarios: Iterable[Dict[str, Any]], filename_prefix: str 144) -> None: 145 """Dumps a list of scenarios to JSON files""" 146 count = 0 147 for scenario in scenarios: 148 filename = "{}{}.json".format(filename_prefix, scenario["name"]) 149 print("Writing file {}".format(filename), file=sys.stderr) 150 with open(filename, "w") as outfile: 151 # The dump file should have {"scenarios" : []} as the top level 152 # element, when embedded in a LoadTest configuration YAML file. 153 json.dump({"scenarios": [scenario]}, outfile, indent=2) 154 count += 1 155 print("Wrote {} scenarios".format(count), file=sys.stderr) 156 157 158def main() -> None: 159 language_choices = sorted(scenario_config.LANGUAGES.keys()) 160 argp = argparse.ArgumentParser(description="Exports scenarios to files.") 161 argp.add_argument( 162 "--export_scenarios", 163 action="store_true", 164 help="Export scenarios to JSON files.", 165 ) 166 argp.add_argument( 167 "--count_scenarios", 168 action="store_true", 169 help="Count scenarios for all test languages.", 170 ) 171 argp.add_argument( 172 "-l", "--language", choices=language_choices, help="Language to export." 173 ) 174 argp.add_argument( 175 "-f", 176 "--filename_prefix", 177 default="scenario_dump_", 178 type=str, 179 help="Prefix for exported JSON file names.", 180 ) 181 argp.add_argument( 182 "-r", 183 "--regex", 184 default=".*", 185 type=str, 186 help="Regex to select scenarios to run.", 187 ) 188 argp.add_argument( 189 "--category", 190 default="all", 191 choices=[ 192 "all", 193 "inproc", 194 "scalable", 195 "smoketest", 196 "sweep", 197 "psm", 198 "dashboard", 199 ], 200 help="Select scenarios for a category of tests.", 201 ) 202 argp.add_argument( 203 "--client_language", 204 default="", 205 choices=language_choices, 206 help="Select only scenarios with a specified client language.", 207 ) 208 argp.add_argument( 209 "--server_language", 210 default="", 211 choices=language_choices, 212 help="Select only scenarios with a specified server language.", 213 ) 214 args = argp.parse_args() 215 216 if args.export_scenarios and not args.language: 217 print( 218 "Dumping scenarios requires a specified language.", file=sys.stderr 219 ) 220 argp.print_usage(file=sys.stderr) 221 return 222 223 if args.export_scenarios: 224 s_filter = scenario_filter( 225 scenario_name_regex=args.regex, 226 category=args.category, 227 client_language=args.client_language, 228 server_language=args.server_language, 229 ) 230 scenarios = gen_scenarios(args.language, s_filter) 231 dump_to_json_files(scenarios, args.filename_prefix) 232 233 if args.count_scenarios: 234 print( 235 "Scenario count for all languages (category: {}):".format( 236 args.category 237 ) 238 ) 239 print( 240 "{:>5} {:16} {:8} {:8} {}".format( 241 "Count", "Language", "Client", "Server", "Categories" 242 ) 243 ) 244 c = collections.Counter(gen_scenario_languages(args.category)) 245 total = 0 246 for (cat, l, cl, sl), count in c.most_common(): 247 print( 248 "{count:5} {l:16} {cl:8} {sl:8} {cat}".format( 249 l=l, cl=cl, sl=sl, count=count, cat=cat 250 ) 251 ) 252 total += count 253 254 print( 255 "\n{:>5} total scenarios (category: {})".format( 256 total, args.category 257 ) 258 ) 259 260 261if __name__ == "__main__": 262 main() 263