blob: a5becefc06bbaa5b195f97f7651a298ced43fcb7 [file] [log] [blame]
#!/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'],
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()