blob: 436557f5691d378eda6373ce1c85a9c7f7435598 [file]
#!/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()