3 # Copyright 2015-2017 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.
35 from conf import settings
36 import core.component_factory as component_factory
37 from core.loader import Loader
38 from testcases import PerformanceTestCase
39 from testcases import IntegrationTestCase
40 from tools import tasks
41 from tools import networkcard
42 from tools import functions
43 from tools.pkt_gen import trafficgen
44 from tools.opnfvdashboard import opnfvdashboard
46 sys.dont_write_bytecode = True
49 'debug': logging.DEBUG,
51 'warning': logging.WARNING,
52 'error': logging.ERROR,
53 'critical': logging.CRITICAL
56 _CURR_DIR = os.path.dirname(os.path.realpath(__file__))
58 _TEMPLATE_RST = {'head' : os.path.join(_CURR_DIR, 'tools/report/report_head.rst'),
59 'foot' : os.path.join(_CURR_DIR, 'tools/report/report_foot.rst'),
60 'final' : 'test_report.rst',
61 'tmp' : os.path.join(_CURR_DIR, 'tools/report/report_tmp_caption.rst')
65 _LOGGER = logging.getLogger()
67 def parse_arguments():
69 Parse command line arguments.
71 class _SplitTestParamsAction(argparse.Action):
73 Parse and split the '--test-params' argument.
75 This expects either 'x=y', 'x=y,z' or 'x' (implicit true)
76 values. For multiple overrides use a ; separated list for
77 e.g. --test-params 'x=z; y=(a,b)'
79 def __call__(self, parser, namespace, values, option_string=None):
82 for param, _, value in re.findall('([^;=]+)(=([^;]+))?', values):
87 # values are passed inside string from CLI, so we must retype them accordingly
89 results[param] = ast.literal_eval(value)
91 # for backward compatibility, we have to accept strings without quotes
92 _LOGGER.warning("Adding missing quotes around string value: %s = %s",
94 results[param] = str(value)
98 setattr(namespace, self.dest, results)
100 class _ValidateFileAction(argparse.Action):
101 """Validate a file can be read from before using it.
103 def __call__(self, parser, namespace, values, option_string=None):
104 if not os.path.isfile(values):
105 raise argparse.ArgumentTypeError(
106 'the path \'%s\' is not a valid path' % values)
107 elif not os.access(values, os.R_OK):
108 raise argparse.ArgumentTypeError(
109 'the path \'%s\' is not accessible' % values)
111 setattr(namespace, self.dest, values)
113 class _ValidateDirAction(argparse.Action):
114 """Validate a directory can be written to before using it.
116 def __call__(self, parser, namespace, values, option_string=None):
117 if not os.path.isdir(values):
118 raise argparse.ArgumentTypeError(
119 'the path \'%s\' is not a valid path' % values)
120 elif not os.access(values, os.W_OK):
121 raise argparse.ArgumentTypeError(
122 'the path \'%s\' is not accessible' % values)
124 setattr(namespace, self.dest, values)
126 def list_logging_levels():
127 """Give a summary of all available logging levels.
129 :return: List of verbosity level names in decreasing order of
132 return sorted(VERBOSITY_LEVELS.keys(),
133 key=lambda x: VERBOSITY_LEVELS[x])
135 parser = argparse.ArgumentParser(prog=__file__, formatter_class=
136 argparse.ArgumentDefaultsHelpFormatter)
137 parser.add_argument('--version', action='version', version='%(prog)s 0.2')
138 parser.add_argument('--list', '--list-tests', action='store_true',
139 help='list all tests and exit')
140 parser.add_argument('--list-trafficgens', action='store_true',
141 help='list all traffic generators and exit')
142 parser.add_argument('--list-collectors', action='store_true',
143 help='list all system metrics loggers and exit')
144 parser.add_argument('--list-vswitches', action='store_true',
145 help='list all system vswitches and exit')
146 parser.add_argument('--list-fwdapps', action='store_true',
147 help='list all system forwarding applications and exit')
148 parser.add_argument('--list-vnfs', action='store_true',
149 help='list all system vnfs and exit')
150 parser.add_argument('--list-loadgens', action='store_true',
151 help='list all background load generators')
152 parser.add_argument('--list-settings', action='store_true',
153 help='list effective settings configuration and exit')
154 parser.add_argument('exact_test_name', nargs='*', help='Exact names of\
155 tests to run. E.g "vsperf phy2phy_tput phy2phy_cont"\
156 runs only the two tests with those exact names.\
157 To run all tests omit both positional args and --tests arg.')
159 group = parser.add_argument_group('test selection options')
160 group.add_argument('-m', '--mode', help='vsperf mode of operation;\
161 Values: "normal" - execute vSwitch, VNF and traffic generator;\
162 "trafficgen" - execute only traffic generator; "trafficgen-off" \
163 - execute vSwitch and VNF; trafficgen-pause - execute vSwitch \
164 and VNF but pause before traffic transmission ', default='normal')
166 group.add_argument('-f', '--test-spec', help='test specification file')
167 group.add_argument('-d', '--test-dir', help='directory containing tests')
168 group.add_argument('-t', '--tests', help='Comma-separated list of terms \
169 indicating tests to run. e.g. "RFC2544,!p2p" - run all tests whose\
170 name contains RFC2544 less those containing "p2p"; "!back2back" - \
171 run all tests except those containing back2back')
172 group.add_argument('--verbosity', choices=list_logging_levels(),
174 group.add_argument('--integration', action='store_true', help='execute integration tests')
175 group.add_argument('--trafficgen', help='traffic generator to use')
176 group.add_argument('--vswitch', help='vswitch implementation to use')
177 group.add_argument('--fwdapp', help='packet forwarding application to use')
178 group.add_argument('--vnf', help='vnf to use')
179 group.add_argument('--loadgen', help='loadgen to use')
180 group.add_argument('--sysmetrics', help='system metrics logger to use')
181 group = parser.add_argument_group('test behavior options')
182 group.add_argument('--xunit', action='store_true',
183 help='enable xUnit-formatted output')
184 group.add_argument('--xunit-dir', action=_ValidateDirAction,
185 help='output directory of xUnit-formatted output')
186 group.add_argument('--load-env', action='store_true',
187 help='enable loading of settings from the environment')
188 group.add_argument('--conf-file', action=_ValidateFileAction,
189 help='settings file')
190 group.add_argument('--test-params', action=_SplitTestParamsAction,
191 help='csv list of test parameters: key=val; e.g. '
192 'TRAFFICGEN_PKT_SIZES=(64,128);TRAFICGEN_DURATION=30; '
193 'GUEST_LOOPBACK=["l2fwd"] ...')
194 group.add_argument('--opnfvpod', help='name of POD in opnfv')
196 args = vars(parser.parse_args())
201 def configure_logging(level):
202 """Configure logging.
204 log_file_default = os.path.join(
205 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_DEFAULT'))
206 log_file_host_cmds = os.path.join(
207 settings.getValue('LOG_DIR'), settings.getValue('LOG_FILE_HOST_CMDS'))
208 log_file_traffic_gen = os.path.join(
209 settings.getValue('LOG_DIR'),
210 settings.getValue('LOG_FILE_TRAFFIC_GEN'))
212 _LOGGER.setLevel(logging.DEBUG)
214 stream_logger = logging.StreamHandler(sys.stdout)
215 stream_logger.setLevel(VERBOSITY_LEVELS[level])
216 stream_logger.setFormatter(logging.Formatter(
217 '[%(levelname)-5s] %(asctime)s : (%(name)s) - %(message)s'))
218 _LOGGER.addHandler(stream_logger)
220 file_logger = logging.FileHandler(filename=log_file_default)
221 file_logger.setLevel(logging.DEBUG)
222 _LOGGER.addHandler(file_logger)
224 class CommandFilter(logging.Filter):
225 """Filter out strings beginning with 'cmd :'"""
226 def filter(self, record):
227 return record.getMessage().startswith(tasks.CMD_PREFIX)
229 class TrafficGenCommandFilter(logging.Filter):
230 """Filter out strings beginning with 'gencmd :'"""
231 def filter(self, record):
232 return record.getMessage().startswith(trafficgen.CMD_PREFIX)
234 cmd_logger = logging.FileHandler(filename=log_file_host_cmds)
235 cmd_logger.setLevel(logging.DEBUG)
236 cmd_logger.addFilter(CommandFilter())
237 _LOGGER.addHandler(cmd_logger)
239 gen_logger = logging.FileHandler(filename=log_file_traffic_gen)
240 gen_logger.setLevel(logging.DEBUG)
241 gen_logger.addFilter(TrafficGenCommandFilter())
242 _LOGGER.addHandler(gen_logger)
245 def apply_filter(tests, tc_filter):
246 """Allow a subset of tests to be conveniently selected
248 :param tests: The list of Tests from which to select.
249 :param tc_filter: A case-insensitive string of comma-separated terms
250 indicating the Tests to select.
251 e.g. 'RFC' - select all tests whose name contains 'RFC'
252 e.g. 'RFC,burst' - select all tests whose name contains 'RFC' or
254 e.g. 'RFC,burst,!p2p' - select all tests whose name contains 'RFC'
255 or 'burst' and from these remove any containing 'p2p'.
256 e.g. '' - empty string selects all tests.
257 :return: A list of the selected Tests.
259 # if negative filter is first we have to start with full list of tests
260 if tc_filter.strip()[0] == '!':
264 if tc_filter is None:
267 for term in [x.strip() for x in tc_filter.lower().split(",")]:
268 if not term or term[0] != '!':
269 # Add matching tests from 'tests' into results
270 result.extend([test for test in tests \
271 if test['Name'].lower().find(term) >= 0])
273 # Term begins with '!' so we remove matching tests
274 result = [test for test in result \
275 if test['Name'].lower().find(term[1:]) < 0]
280 def check_and_set_locale():
281 """ Function will check locale settings. In case, that it isn't configured
282 properly, then default values specified by DEFAULT_LOCALE will be used.
285 system_locale = locale.getdefaultlocale()
286 if None in system_locale:
287 os.environ['LC_ALL'] = settings.getValue('DEFAULT_LOCALE')
288 _LOGGER.warning("Locale was not properly configured. Default values were set. Old locale: %s, New locale: %s",
289 system_locale, locale.getdefaultlocale())
291 def get_vswitch_names(rst_files):
292 """ Function will return a list of vSwitches detected in given ``rst_files``.
294 vswitch_names = set()
297 output = subprocess.check_output(['grep', '-h', '^* vSwitch'] + rst_files).decode().splitlines()
299 match = re.search(r'^\* vSwitch: ([^,]+)', str(line))
301 vswitch_names.add(match.group(1))
303 if len(vswitch_names):
304 return list(vswitch_names)
306 except subprocess.CalledProcessError:
307 _LOGGER.warning('Cannot detect vSwitches used during testing.')
309 # fallback to the default value
313 """ Function will return a Jenkins job ID environment variable.
317 build_tag = os.environ['BUILD_TAG']
320 _LOGGER.warning('Cannot detect Jenkins job ID')
325 def generate_final_report():
326 """ Function will check if partial test results are available
327 and generates final report in rst format.
330 path = settings.getValue('RESULTS_PATH')
331 # check if there are any results in rst format
332 rst_results = glob.glob(os.path.join(path, 'result*rst'))
333 pkt_processors = get_vswitch_names(rst_results)
336 test_report = os.path.join(path, '{}_{}'.format('_'.join(pkt_processors), _TEMPLATE_RST['final']))
337 # create report caption directly - it is not worth to execute jinja machinery
338 report_caption = '{}\n{} {}\n{}\n\n'.format(
339 '============================================================',
340 'Performance report for',
341 ', '.join(pkt_processors),
342 '============================================================')
344 with open(_TEMPLATE_RST['tmp'], 'w') as file_:
345 file_.write(report_caption)
347 retval = subprocess.call('cat {} {} {} {} > {}'.format(_TEMPLATE_RST['tmp'], _TEMPLATE_RST['head'],
348 ' '.join(rst_results), _TEMPLATE_RST['foot'],
349 test_report), shell=True)
350 if retval == 0 and os.path.isfile(test_report):
351 _LOGGER.info('Overall test report written to "%s"', test_report)
353 _LOGGER.error('Generation of overall test report has failed.')
355 # remove temporary file
356 os.remove(_TEMPLATE_RST['tmp'])
358 except subprocess.CalledProcessError:
359 _LOGGER.error('Generatrion of overall test report has failed.')
362 def enable_sriov(nic_list):
363 """ Enable SRIOV for given enhanced PCI IDs
365 :param nic_list: A list of enhanced PCI IDs
367 # detect if sriov is required
370 if networkcard.is_sriov_nic(nic):
371 tmp_nic = nic.split('|')
372 if tmp_nic[0] in sriov_nic:
373 if int(tmp_nic[1][2:]) > sriov_nic[tmp_nic[0]]:
374 sriov_nic[tmp_nic[0]] = int(tmp_nic[1][2:])
376 sriov_nic.update({tmp_nic[0] : int(tmp_nic[1][2:])})
378 # sriov is required for some NICs
380 for nic in sriov_nic:
381 # check if SRIOV is supported and enough virt interfaces are available
382 if not networkcard.is_sriov_supported(nic) \
383 or networkcard.get_sriov_numvfs(nic) <= sriov_nic[nic]:
384 # if not, enable and set appropriate number of VFs
385 if not networkcard.set_sriov_numvfs(nic, sriov_nic[nic] + 1):
386 raise RuntimeError('SRIOV cannot be enabled for NIC {}'.format(nic))
388 _LOGGER.debug("SRIOV enabled for NIC %s", nic)
390 # WORKAROUND: it has been observed with IXGBE(VF) driver,
391 # that NIC doesn't correclty dispatch traffic to VFs based
392 # on their MAC address. Unbind and bind to the same driver
394 networkcard.reinit_vfs(nic)
396 # After SRIOV is enabled it takes some time until network drivers
397 # properly initialize all cards.
398 # Wait also in case, that SRIOV was already configured as it can be
399 # configured automatically just before vsperf execution.
407 def disable_sriov(nic_list):
408 """ Disable SRIOV for given PCI IDs
410 :param nic_list: A list of enhanced PCI IDs
413 if networkcard.is_sriov_nic(nic):
414 if not networkcard.set_sriov_numvfs(nic.split('|')[0], 0):
415 raise RuntimeError('SRIOV cannot be disabled for NIC {}'.format(nic))
417 _LOGGER.debug("SRIOV disabled for NIC %s", nic.split('|')[0])
420 def handle_list_options(args):
421 """ Process --list cli arguments if needed
423 :param args: A dictionary with all CLI arguments
425 if args['list_trafficgens']:
426 print(Loader().get_trafficgens_printable())
429 if args['list_collectors']:
430 print(Loader().get_collectors_printable())
433 if args['list_vswitches']:
434 print(Loader().get_vswitches_printable())
437 if args['list_vnfs']:
438 print(Loader().get_vnfs_printable())
441 if args['list_fwdapps']:
442 print(Loader().get_pktfwds_printable())
445 if args['list_loadgens']:
446 print(Loader().get_loadgens_printable())
449 if args['list_settings']:
455 if args['integration']:
456 testcases = settings.getValue('INTEGRATION_TESTS')
458 testcases = settings.getValue('PERFORMANCE_TESTS')
460 print("Available Tests:")
461 print("================")
463 for test in testcases:
464 print('* %-30s %s' % ('%s:' % test['Name'], test['Description']))
468 def vsperf_finalize():
469 """ Clean up before exit
471 # remove directory if no result files were created
473 results_path = settings.getValue('RESULTS_PATH')
474 if os.path.exists(results_path):
475 files_list = os.listdir(results_path)
477 _LOGGER.info("Removing empty result directory: " + results_path)
478 shutil.rmtree(results_path)
479 except AttributeError:
480 # skip it if parameter doesn't exist
483 # disable SRIOV if needed
485 if settings.getValue('SRIOV_ENABLED'):
486 disable_sriov(settings.getValue('WHITELIST_NICS_ORIG'))
487 except AttributeError:
488 # skip it if parameter doesn't exist
492 class MockTestCase(unittest.TestCase):
493 """Allow use of xmlrunner to generate Jenkins compatible output without
494 using xmlrunner to actually run tests.
497 suite = unittest.TestSuite()
498 suite.addTest(MockTestCase('Test1 passed ', True, 'Test1'))
499 suite.addTest(MockTestCase('Test2 failed because...', False, 'Test2'))
500 xmlrunner.XMLTestRunner(...).run(suite)
503 def __init__(self, msg, is_pass, test_name):
506 self.is_pass = is_pass
508 #dynamically create a test method with the right name
509 #but point the method at our generic test method
510 setattr(MockTestCase, test_name, self.generic_test)
512 super(MockTestCase, self).__init__(test_name)
514 def generic_test(self):
515 """Provide a generic function that raises or not based
516 on how self.is_pass was set in the constructor"""
517 self.assertTrue(self.is_pass, self.msg)
519 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
523 args = parse_arguments()
527 settings.load_from_dir(os.path.join(_CURR_DIR, 'conf'))
529 # Load non performance/integration tests
530 if args['integration']:
531 settings.load_from_dir(os.path.join(_CURR_DIR, 'conf/integration'))
533 # load command line parameters first in case there are settings files
535 settings.load_from_dict(args)
537 if args['conf_file']:
538 settings.load_from_file(args['conf_file'])
541 settings.load_from_env()
543 # reload command line parameters since these should take higher priority
544 # than both a settings file and environment variables
545 settings.load_from_dict(args)
547 settings.setValue('mode', args['mode'])
549 # set dpdk and ovs paths according to VNF and VSWITCH
550 if settings.getValue('mode') != 'trafficgen':
551 functions.settings_update_paths()
553 # if required, handle list-* operations
554 handle_list_options(args)
556 configure_logging(settings.getValue('VERBOSITY'))
558 # check and fix locale
559 check_and_set_locale()
561 # configure trafficgens
562 if args['trafficgen']:
563 trafficgens = Loader().get_trafficgens()
564 if args['trafficgen'] not in trafficgens:
565 _LOGGER.error('There are no trafficgens matching \'%s\' found in'
566 ' \'%s\'. Exiting...', args['trafficgen'],
567 settings.getValue('TRAFFICGEN_DIR'))
570 # configuration validity checks
572 vswitch_none = args['vswitch'].strip().lower() == 'none'
574 settings.setValue('VSWITCH', 'none')
576 vswitches = Loader().get_vswitches()
577 if args['vswitch'] not in vswitches:
578 _LOGGER.error('There are no vswitches matching \'%s\' found in'
579 ' \'%s\'. Exiting...', args['vswitch'],
580 settings.getValue('VSWITCH_DIR'))
584 settings.setValue('PKTFWD', args['fwdapp'])
585 fwdapps = Loader().get_pktfwds()
586 if args['fwdapp'] not in fwdapps:
587 _LOGGER.error('There are no forwarding application'
588 ' matching \'%s\' found in'
589 ' \'%s\'. Exiting...', args['fwdapp'],
590 settings.getValue('PKTFWD_DIR'))
594 vnfs = Loader().get_vnfs()
595 if args['vnf'] not in vnfs:
596 _LOGGER.error('there are no vnfs matching \'%s\' found in'
597 ' \'%s\'. exiting...', args['vnf'],
598 settings.getValue('VNF_DIR'))
602 loadgens = Loader().get_loadgens()
603 if args['loadgen'] not in loadgens:
604 _LOGGER.error('There are no loadgens matching \'%s\' found in'
605 ' \'%s\'. Exiting...', args['loadgen'],
606 settings.getValue('LOADGEN_DIR'))
609 if args['exact_test_name'] and args['tests']:
610 _LOGGER.error("Cannot specify tests with both positional args and --test.")
613 # modify NIC configuration to decode enhanced PCI IDs
614 wl_nics_orig = list(networkcard.check_pci(pci) for pci in settings.getValue('WHITELIST_NICS'))
615 settings.setValue('WHITELIST_NICS_ORIG', wl_nics_orig)
617 # sriov handling is performed on checked/expanded PCI IDs
618 settings.setValue('SRIOV_ENABLED', enable_sriov(wl_nics_orig))
621 for nic in wl_nics_orig:
622 tmp_nic = networkcard.get_nic_info(nic)
624 nic_list.append({'pci' : tmp_nic,
625 'type' : 'vf' if networkcard.get_sriov_pf(tmp_nic) else 'pf',
626 'mac' : networkcard.get_mac(tmp_nic),
627 'driver' : networkcard.get_driver(tmp_nic),
628 'device' : networkcard.get_device_name(tmp_nic)})
631 raise RuntimeError("Invalid network card PCI ID: '{}'".format(nic))
633 settings.setValue('NICS', nic_list)
634 # for backward compatibility
635 settings.setValue('WHITELIST_NICS', list(nic['pci'] for nic in nic_list))
637 # generate results directory name
638 date = datetime.datetime.fromtimestamp(time.time())
639 results_dir = "results_" + date.strftime('%Y-%m-%d_%H-%M-%S')
640 results_path = os.path.join(settings.getValue('LOG_DIR'), results_dir)
641 settings.setValue('RESULTS_PATH', results_path)
643 # create results directory
644 if not os.path.exists(results_path):
645 _LOGGER.info("Creating result directory: " + results_path)
646 os.makedirs(results_path)
648 if settings.getValue('mode') == 'trafficgen':
649 # execute only traffic generator
650 _LOGGER.debug("Executing traffic generator:")
652 # set traffic details, so they can be passed to traffic ctl
653 traffic = copy.deepcopy(settings.getValue('TRAFFIC'))
655 traffic = functions.check_traffic(traffic)
657 traffic_ctl = component_factory.create_traffic(
658 traffic['traffic_type'],
659 loader.get_trafficgen_class())
661 traffic_ctl.send_traffic(traffic)
662 _LOGGER.debug("Traffic Results:")
663 traffic_ctl.print_results()
665 # write results into CSV file
666 result_file = os.path.join(results_path, "result.csv")
667 PerformanceTestCase.write_result_to_file(traffic_ctl.get_results(), result_file)
670 if args['integration']:
671 testcases = settings.getValue('INTEGRATION_TESTS')
673 testcases = settings.getValue('PERFORMANCE_TESTS')
675 if args['exact_test_name']:
676 exact_names = args['exact_test_name']
677 # positional args => exact matches only
678 selected_tests = [test for test in testcases if test['Name'] in exact_names]
680 # --tests => apply filter to select requested tests
681 selected_tests = apply_filter(testcases, args['tests'])
683 # Default - run all tests
684 selected_tests = testcases
686 if not len(selected_tests):
687 _LOGGER.error("No tests matched --tests option or positional args. Done.")
692 # Add pylint exception: Redefinition of test type from
693 # testcases.integration.IntegrationTestCase to testcases.performance.PerformanceTestCase
694 # pylint: disable=redefined-variable-type
695 suite = unittest.TestSuite()
696 for cfg in selected_tests:
697 test_name = cfg.get('Name', '<Name not set>')
699 if args['integration']:
700 test = IntegrationTestCase(cfg)
702 test = PerformanceTestCase(cfg)
704 suite.addTest(MockTestCase('', True, test.name))
705 # pylint: disable=broad-except
706 except (Exception) as ex:
707 _LOGGER.exception("Failed to run test: %s", test_name)
708 suite.addTest(MockTestCase(str(ex), False, test_name))
709 _LOGGER.info("Continuing with next test...")
711 # generate final rst report with results of all executed TCs
712 generate_final_report()
714 if settings.getValue('XUNIT'):
715 xmlrunner.XMLTestRunner(
716 output=settings.getValue('XUNIT_DIR'), outsuffix="",
717 verbosity=0).run(suite)
720 pod_name = args['opnfvpod']
721 installer_name = str(settings.getValue('OPNFV_INSTALLER')).lower()
722 opnfv_url = settings.getValue('OPNFV_URL')
723 pkg_list = settings.getValue('PACKAGE_LIST')
725 int_data = {'pod': pod_name,
726 'build_tag': get_build_tag(),
727 'installer': installer_name,
728 'pkg_list': pkg_list,
730 # pass vswitch name from configuration to be used for failed
731 # TCs; In case of successful TCs it is safer to use vswitch
732 # name from CSV as TC can override global configuration
733 'vswitch': str(settings.getValue('VSWITCH')).lower()}
734 tc_names = [tc['Name'] for tc in selected_tests]
735 opnfvdashboard.results2opnfv_dashboard(tc_names, results_path, int_data)
737 # cleanup before exit
740 if __name__ == "__main__":