scenario_config_exporter.py 8.6 KB

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