• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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