3 # Copyright 2015 Intel Corporation.
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
17 """VSPERF main script.
30 sys.dont_write_bytecode = True
32 from conf import settings
33 from core.loader import Loader
34 from testcases import TestCase
35 from tools import tasks
36 from tools.collectors import collector
37 from tools.pkt_gen import trafficgen
40 'debug': logging.DEBUG,
42 'warning': logging.WARNING,
43 'error': logging.ERROR,
44 'critical': logging.CRITICAL
48 def parse_arguments():
50 Parse command line arguments.
52 class _SplitTestParamsAction(argparse.Action):
54 Parse and split the '--test-params' argument.
56 This expects either 'x=y' or 'x' (implicit true) values.
58 def __call__(self, parser, namespace, values, option_string=None):
61 for value in values.split(';'):
62 result = [key.strip() for key in value.split('=')]
64 results[result[0]] = True
65 elif len(result) == 2:
66 results[result[0]] = result[1]
68 raise argparse.ArgumentTypeError(
69 'expected \'%s\' to be of format \'key=val\' or'
72 setattr(namespace, self.dest, results)
74 class _ValidateFileAction(argparse.Action):
75 """Validate a file can be read from before using it.
77 def __call__(self, parser, namespace, values, option_string=None):
78 if not os.path.isfile(values):
79 raise argparse.ArgumentTypeError(
80 'the path \'%s\' is not a valid path' % values)
81 elif not os.access(values, os.R_OK):
82 raise argparse.ArgumentTypeError(
83 'the path \'%s\' is not accessible' % values)
85 setattr(namespace, self.dest, values)
87 class _ValidateDirAction(argparse.Action):
88 """Validate a directory can be written to before using it.
90 def __call__(self, parser, namespace, values, option_string=None):
91 if not os.path.isdir(values):
92 raise argparse.ArgumentTypeError(
93 'the path \'%s\' is not a valid path' % values)
94 elif not os.access(values, os.W_OK):
95 raise argparse.ArgumentTypeError(
96 'the path \'%s\' is not accessible' % values)
98 setattr(namespace, self.dest, values)
100 def list_logging_levels():
101 """Give a summary of all available logging levels.
103 :return: List of verbosity level names in decreasing order of
106 return sorted(VERBOSITY_LEVELS.keys(),
107 key=lambda x: VERBOSITY_LEVELS[x])
109 parser = argparse.ArgumentParser(prog=__file__, formatter_class=
110 argparse.ArgumentDefaultsHelpFormatter)
111 parser.add_argument('--version', action='version', version='%(prog)s 0.2')
112 parser.add_argument('--list', '--list-tests', action='store_true',
113 help='list all tests and exit')
114 parser.add_argument('--list-trafficgens', action='store_true',
115 help='list all traffic generators and exit')
116 parser.add_argument('--list-collectors', action='store_true',
117 help='list all system metrics loggers and exit')
118 parser.add_argument('--list-vswitches', action='store_true',
119 help='list all system vswitches and exit')
120 parser.add_argument('--list-settings', action='store_true',
121 help='list effective settings configuration and exit')
122 parser.add_argument('test', nargs='*', help='test specification(s)')
124 group = parser.add_argument_group('test selection options')
125 group.add_argument('-f', '--test-spec', help='test specification file')
126 group.add_argument('-d', '--test-dir', help='directory containing tests')
127 group.add_argument('-t', '--tests', help='Comma-separated list of terms \
128 indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
129 name contains RFC2544 less those containing "p2p"')
130 group.add_argument('--verbosity', choices=list_logging_levels(),
132 group.add_argument('--trafficgen', help='traffic generator to use')
133 group.add_argument('--vswitch', help='vswitch implementation to use')
134 group.add_argument('--sysmetrics', help='system metrics logger to use')
135 group = parser.add_argument_group('test behavior options')
136 group.add_argument('--xunit', action='store_true',
137 help='enable xUnit-formatted output')
138 group.add_argument('--xunit-dir', action=_ValidateDirAction,
139 help='output directory of xUnit-formatted output')
140 group.add_argument('--load-env', action='store_true',
141 help='enable loading of settings from the environment')
142 group.add_argument('--conf-file', action=_ValidateFileAction,
143 help='settings file')
144 group.add_argument('--test-params', action=_SplitTestParamsAction,
145 help='csv list of test parameters: key=val;...')
147 args = vars(parser.parse_args())
152 def configure_logging(level):
153 """Configure logging.
155 log_file_default = os.path.join(
156 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
157 log_file_host_cmds = os.path.join(
158 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
159 log_file_traffic_gen = os.path.join(
160 settings.getValue('LOG_DIR'),
161 settings.getValue('LOG_FILE_TRAFFIC_GEN'))
162 log_file_sys_metrics = os.path.join(
163 settings.getValue('LOG_DIR'),
164 settings.getValue('LOG_FILE_SYS_METRICS'))
166 logger = logging.getLogger()
167 logger.setLevel(logging.DEBUG)
169 stream_logger = logging.StreamHandler(sys.stdout)
170 stream_logger.setLevel(VERBOSITY_LEVELS[level])
171 stream_logger.setFormatter(logging.Formatter(
172 '[%(levelname)s] %(asctime)s : (%(name)s) - %(message)s'))
173 logger.addHandler(stream_logger)
175 file_logger = logging.FileHandler(filename=log_file_default)
176 file_logger.setLevel(logging.DEBUG)
177 logger.addHandler(file_logger)
179 class CommandFilter(logging.Filter):
180 """Filter out strings beginning with 'cmd :'"""
181 def filter(self, record):
182 return record.getMessage().startswith(tasks.CMD_PREFIX)
184 class TrafficGenCommandFilter(logging.Filter):
185 """Filter out strings beginning with 'gencmd :'"""
186 def filter(self, record):
187 return record.getMessage().startswith(trafficgen.CMD_PREFIX)
189 class SystemMetricsCommandFilter(logging.Filter):
190 """Filter out strings beginning with 'gencmd :'"""
191 def filter(self, record):
192 return record.getMessage().startswith(collector.CMD_PREFIX)
194 cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
195 cmd_logger.setLevel(logging.DEBUG)
196 cmd_logger.addFilter(CommandFilter())
197 logger.addHandler(cmd_logger)
199 gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
200 gen_logger.setLevel(logging.DEBUG)
201 gen_logger.addFilter(TrafficGenCommandFilter())
202 logger.addHandler(gen_logger)
204 metrics_logger = logging.FileHandler(filename=log_file_sys_metrics)
205 metrics_logger.setLevel(logging.DEBUG)
206 metrics_logger.addFilter(SystemMetricsCommandFilter())
207 logger.addHandler(metrics_logger)
210 def apply_filter(tests, tc_filter):
211 """Allow a subset of tests to be conveniently selected
213 :param tests: The list of Tests from which to select.
214 :param tc_filter: A case-insensitive string of comma-separated terms
215 indicating the Tests to select.
216 e.g. 'RFC' - select all tests whose name contains 'RFC'
217 e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
219 e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
220 or 'burst' and from these remove any containing 'p2p'.
221 e.g. '' - empty string selects all tests.
222 :return: A list of the selected Tests.
225 if tc_filter is None:
228 for term in [x.strip() for x in tc_filter.lower().split(",")]:
229 if not term or term[0] != '!':
230 # Add matching tests from 'tests' into results
231 result.extend([test for test in tests \
232 if test.name.lower().find(term) >= 0])
234 # Term begins with '!' so we remove matching tests
235 result = [test for test in result \
236 if test.name.lower().find(term[1:]) < 0]
241 class MockTestCase(unittest.TestCase):
242 """Allow use of xmlrunner to generate Jenkins compatible output without
243 using xmlrunner to actually run tests.
246 suite = unittest.TestSuite()
247 suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
248 suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
249 xmlrunner.XMLTestRunner(...).run(suite)
252 def __init__(self, msg, is_pass, test_name):
255 self.is_pass = is_pass
257 #dynamically create a test method with the right name
258 #but point the method at our generic test method
259 setattr(MockTestCase, test_name, self.generic_test)
261 super(MockTestCase, self).__init__(test_name)
263 def generic_test(self):
264 """Provide a generic function that raises or not based
265 on how self.is_pass was set in the constructor"""
266 self.assertTrue(self.is_pass, self.msg)
272 args = parse_arguments()
276 settings.load_from_dir('conf')
278 # load command line parameters first in case there are settings files
280 settings.load_from_dict(args)
282 if args['conf_file']:
283 settings.load_from_file(args['conf_file'])
286 settings.load_from_env()
288 # reload command line parameters since these should take higher priority
289 # than both a settings file and environment variables
290 settings.load_from_dict(args)
292 configure_logging(settings.getValue('VERBOSITY'))
293 logger = logging.getLogger()
295 # configure trafficgens
297 if args['trafficgen']:
298 trafficgens = Loader().get_trafficgens()
299 if args['trafficgen'] not in trafficgens:
300 logging.error('There are no trafficgens matching \'%s\' found in'
301 ' \'%s\'. Exiting...', args['trafficgen'],
302 settings.getValue('TRAFFICGEN_DIR'))
307 vswitches = Loader().get_vswitches()
308 if args['vswitch'] not in vswitches:
309 logging.error('There are no vswitches matching \'%s\' found in'
310 ' \'%s\'. Exiting...', args['vswitch'],
311 settings.getValue('VSWITCH_DIR'))
316 # generate results directory name
317 date = datetime.datetime.fromtimestamp(time.time())
318 results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
319 results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
322 testcases = settings.getValue('PERFORMANCE_TESTS')
324 for cfg in testcases:
326 all_tests.append(TestCase(cfg, results_path))
327 except (Exception) as _:
328 logger.exception("Failed to create test: %s",
329 cfg.get('Name', '<Name not set>'))
332 # TODO(BOM) Apply filter to select requested tests
333 all_tests = apply_filter(all_tests, args['tests'])
335 # if required, handle list-* operations
338 print("Available Tests:")
340 for test in all_tests:
341 print('* %-18s%s' % ('%s:' % test.name, test.desc))
344 if args['list_trafficgens']:
345 print(Loader().get_trafficgens_printable())
348 if args['list_collectors']:
349 print(Loader().get_collectors_printable())
352 if args['list_vswitches']:
353 print(Loader().get_vswitches_printable())
356 if args['list_settings']:
360 # create results directory
361 if not os.path.exists(results_dir):
362 logger.info("Creating result directory: " + results_path)
363 os.makedirs(results_path)
365 suite = unittest.TestSuite()
368 for test in all_tests:
371 suite.addTest(MockTestCase('', True, test.name))
372 #pylint: disable=broad-except
373 except (Exception) as ex:
374 logger.exception("Failed to run test: %s", test.name)
375 suite.addTest(MockTestCase(str(ex), False, test.name))
376 logger.info("Continuing with next test...")
378 if settings.getValue('XUNIT'):
379 xmlrunner.XMLTestRunner(
380 output=settings.getValue('XUNIT_DIR'), outsuffix="",
381 verbosity=0).run(suite)
383 #remove directory if no result files were created.
384 if os.path.exists(results_path):
385 if os.listdir(results_path) == []:
386 shutil.rmtree(results_path)
388 if __name__ == "__main__":