1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0 3*4882a593Smuzhiyun 4*4882a593Smuzhiyun""" 5*4882a593Smuzhiyuntdc.py - Linux tc (Traffic Control) unit test driver 6*4882a593Smuzhiyun 7*4882a593SmuzhiyunCopyright (C) 2017 Lucas Bates <lucasb@mojatatu.com> 8*4882a593Smuzhiyun""" 9*4882a593Smuzhiyun 10*4882a593Smuzhiyunimport re 11*4882a593Smuzhiyunimport os 12*4882a593Smuzhiyunimport sys 13*4882a593Smuzhiyunimport argparse 14*4882a593Smuzhiyunimport importlib 15*4882a593Smuzhiyunimport json 16*4882a593Smuzhiyunimport subprocess 17*4882a593Smuzhiyunimport time 18*4882a593Smuzhiyunimport traceback 19*4882a593Smuzhiyunfrom collections import OrderedDict 20*4882a593Smuzhiyunfrom string import Template 21*4882a593Smuzhiyun 22*4882a593Smuzhiyunfrom tdc_config import * 23*4882a593Smuzhiyunfrom tdc_helper import * 24*4882a593Smuzhiyun 25*4882a593Smuzhiyunimport TdcPlugin 26*4882a593Smuzhiyunfrom TdcResults import * 27*4882a593Smuzhiyun 28*4882a593Smuzhiyunclass PluginDependencyException(Exception): 29*4882a593Smuzhiyun def __init__(self, missing_pg): 30*4882a593Smuzhiyun self.missing_pg = missing_pg 31*4882a593Smuzhiyun 32*4882a593Smuzhiyunclass PluginMgrTestFail(Exception): 33*4882a593Smuzhiyun def __init__(self, stage, output, message): 34*4882a593Smuzhiyun self.stage = stage 35*4882a593Smuzhiyun self.output = output 36*4882a593Smuzhiyun self.message = message 37*4882a593Smuzhiyun 38*4882a593Smuzhiyunclass PluginMgr: 39*4882a593Smuzhiyun def __init__(self, argparser): 40*4882a593Smuzhiyun super().__init__() 41*4882a593Smuzhiyun self.plugins = {} 42*4882a593Smuzhiyun self.plugin_instances = [] 43*4882a593Smuzhiyun self.failed_plugins = {} 44*4882a593Smuzhiyun self.argparser = argparser 45*4882a593Smuzhiyun 46*4882a593Smuzhiyun # TODO, put plugins in order 47*4882a593Smuzhiyun plugindir = os.getenv('TDC_PLUGIN_DIR', './plugins') 48*4882a593Smuzhiyun for dirpath, dirnames, filenames in os.walk(plugindir): 49*4882a593Smuzhiyun for fn in filenames: 50*4882a593Smuzhiyun if (fn.endswith('.py') and 51*4882a593Smuzhiyun not fn == '__init__.py' and 52*4882a593Smuzhiyun not fn.startswith('#') and 53*4882a593Smuzhiyun not fn.startswith('.#')): 54*4882a593Smuzhiyun mn = fn[0:-3] 55*4882a593Smuzhiyun foo = importlib.import_module('plugins.' + mn) 56*4882a593Smuzhiyun self.plugins[mn] = foo 57*4882a593Smuzhiyun self.plugin_instances.append(foo.SubPlugin()) 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun def load_plugin(self, pgdir, pgname): 60*4882a593Smuzhiyun pgname = pgname[0:-3] 61*4882a593Smuzhiyun foo = importlib.import_module('{}.{}'.format(pgdir, pgname)) 62*4882a593Smuzhiyun self.plugins[pgname] = foo 63*4882a593Smuzhiyun self.plugin_instances.append(foo.SubPlugin()) 64*4882a593Smuzhiyun self.plugin_instances[-1].check_args(self.args, None) 65*4882a593Smuzhiyun 66*4882a593Smuzhiyun def get_required_plugins(self, testlist): 67*4882a593Smuzhiyun ''' 68*4882a593Smuzhiyun Get all required plugins from the list of test cases and return 69*4882a593Smuzhiyun all unique items. 70*4882a593Smuzhiyun ''' 71*4882a593Smuzhiyun reqs = [] 72*4882a593Smuzhiyun for t in testlist: 73*4882a593Smuzhiyun try: 74*4882a593Smuzhiyun if 'requires' in t['plugins']: 75*4882a593Smuzhiyun if isinstance(t['plugins']['requires'], list): 76*4882a593Smuzhiyun reqs.extend(t['plugins']['requires']) 77*4882a593Smuzhiyun else: 78*4882a593Smuzhiyun reqs.append(t['plugins']['requires']) 79*4882a593Smuzhiyun except KeyError: 80*4882a593Smuzhiyun continue 81*4882a593Smuzhiyun reqs = get_unique_item(reqs) 82*4882a593Smuzhiyun return reqs 83*4882a593Smuzhiyun 84*4882a593Smuzhiyun def load_required_plugins(self, reqs, parser, args, remaining): 85*4882a593Smuzhiyun ''' 86*4882a593Smuzhiyun Get all required plugins from the list of test cases and load any plugin 87*4882a593Smuzhiyun that is not already enabled. 88*4882a593Smuzhiyun ''' 89*4882a593Smuzhiyun pgd = ['plugin-lib', 'plugin-lib-custom'] 90*4882a593Smuzhiyun pnf = [] 91*4882a593Smuzhiyun 92*4882a593Smuzhiyun for r in reqs: 93*4882a593Smuzhiyun if r not in self.plugins: 94*4882a593Smuzhiyun fname = '{}.py'.format(r) 95*4882a593Smuzhiyun source_path = [] 96*4882a593Smuzhiyun for d in pgd: 97*4882a593Smuzhiyun pgpath = '{}/{}'.format(d, fname) 98*4882a593Smuzhiyun if os.path.isfile(pgpath): 99*4882a593Smuzhiyun source_path.append(pgpath) 100*4882a593Smuzhiyun if len(source_path) == 0: 101*4882a593Smuzhiyun print('ERROR: unable to find required plugin {}'.format(r)) 102*4882a593Smuzhiyun pnf.append(fname) 103*4882a593Smuzhiyun continue 104*4882a593Smuzhiyun elif len(source_path) > 1: 105*4882a593Smuzhiyun print('WARNING: multiple copies of plugin {} found, using version found') 106*4882a593Smuzhiyun print('at {}'.format(source_path[0])) 107*4882a593Smuzhiyun pgdir = source_path[0] 108*4882a593Smuzhiyun pgdir = pgdir.split('/')[0] 109*4882a593Smuzhiyun self.load_plugin(pgdir, fname) 110*4882a593Smuzhiyun if len(pnf) > 0: 111*4882a593Smuzhiyun raise PluginDependencyException(pnf) 112*4882a593Smuzhiyun 113*4882a593Smuzhiyun parser = self.call_add_args(parser) 114*4882a593Smuzhiyun (args, remaining) = parser.parse_known_args(args=remaining, namespace=args) 115*4882a593Smuzhiyun return args 116*4882a593Smuzhiyun 117*4882a593Smuzhiyun def call_pre_suite(self, testcount, testidlist): 118*4882a593Smuzhiyun for pgn_inst in self.plugin_instances: 119*4882a593Smuzhiyun pgn_inst.pre_suite(testcount, testidlist) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyun def call_post_suite(self, index): 122*4882a593Smuzhiyun for pgn_inst in reversed(self.plugin_instances): 123*4882a593Smuzhiyun pgn_inst.post_suite(index) 124*4882a593Smuzhiyun 125*4882a593Smuzhiyun def call_pre_case(self, caseinfo, *, test_skip=False): 126*4882a593Smuzhiyun for pgn_inst in self.plugin_instances: 127*4882a593Smuzhiyun try: 128*4882a593Smuzhiyun pgn_inst.pre_case(caseinfo, test_skip) 129*4882a593Smuzhiyun except Exception as ee: 130*4882a593Smuzhiyun print('exception {} in call to pre_case for {} plugin'. 131*4882a593Smuzhiyun format(ee, pgn_inst.__class__)) 132*4882a593Smuzhiyun print('test_ordinal is {}'.format(test_ordinal)) 133*4882a593Smuzhiyun print('testid is {}'.format(caseinfo['id'])) 134*4882a593Smuzhiyun raise 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun def call_post_case(self): 137*4882a593Smuzhiyun for pgn_inst in reversed(self.plugin_instances): 138*4882a593Smuzhiyun pgn_inst.post_case() 139*4882a593Smuzhiyun 140*4882a593Smuzhiyun def call_pre_execute(self): 141*4882a593Smuzhiyun for pgn_inst in self.plugin_instances: 142*4882a593Smuzhiyun pgn_inst.pre_execute() 143*4882a593Smuzhiyun 144*4882a593Smuzhiyun def call_post_execute(self): 145*4882a593Smuzhiyun for pgn_inst in reversed(self.plugin_instances): 146*4882a593Smuzhiyun pgn_inst.post_execute() 147*4882a593Smuzhiyun 148*4882a593Smuzhiyun def call_add_args(self, parser): 149*4882a593Smuzhiyun for pgn_inst in self.plugin_instances: 150*4882a593Smuzhiyun parser = pgn_inst.add_args(parser) 151*4882a593Smuzhiyun return parser 152*4882a593Smuzhiyun 153*4882a593Smuzhiyun def call_check_args(self, args, remaining): 154*4882a593Smuzhiyun for pgn_inst in self.plugin_instances: 155*4882a593Smuzhiyun pgn_inst.check_args(args, remaining) 156*4882a593Smuzhiyun 157*4882a593Smuzhiyun def call_adjust_command(self, stage, command): 158*4882a593Smuzhiyun for pgn_inst in self.plugin_instances: 159*4882a593Smuzhiyun command = pgn_inst.adjust_command(stage, command) 160*4882a593Smuzhiyun return command 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun def set_args(self, args): 163*4882a593Smuzhiyun self.args = args 164*4882a593Smuzhiyun 165*4882a593Smuzhiyun @staticmethod 166*4882a593Smuzhiyun def _make_argparser(args): 167*4882a593Smuzhiyun self.argparser = argparse.ArgumentParser( 168*4882a593Smuzhiyun description='Linux TC unit tests') 169*4882a593Smuzhiyun 170*4882a593Smuzhiyundef replace_keywords(cmd): 171*4882a593Smuzhiyun """ 172*4882a593Smuzhiyun For a given executable command, substitute any known 173*4882a593Smuzhiyun variables contained within NAMES with the correct values 174*4882a593Smuzhiyun """ 175*4882a593Smuzhiyun tcmd = Template(cmd) 176*4882a593Smuzhiyun subcmd = tcmd.safe_substitute(NAMES) 177*4882a593Smuzhiyun return subcmd 178*4882a593Smuzhiyun 179*4882a593Smuzhiyun 180*4882a593Smuzhiyundef exec_cmd(args, pm, stage, command): 181*4882a593Smuzhiyun """ 182*4882a593Smuzhiyun Perform any required modifications on an executable command, then run 183*4882a593Smuzhiyun it in a subprocess and return the results. 184*4882a593Smuzhiyun """ 185*4882a593Smuzhiyun if len(command.strip()) == 0: 186*4882a593Smuzhiyun return None, None 187*4882a593Smuzhiyun if '$' in command: 188*4882a593Smuzhiyun command = replace_keywords(command) 189*4882a593Smuzhiyun 190*4882a593Smuzhiyun command = pm.call_adjust_command(stage, command) 191*4882a593Smuzhiyun if args.verbose > 0: 192*4882a593Smuzhiyun print('command "{}"'.format(command)) 193*4882a593Smuzhiyun proc = subprocess.Popen(command, 194*4882a593Smuzhiyun shell=True, 195*4882a593Smuzhiyun stdout=subprocess.PIPE, 196*4882a593Smuzhiyun stderr=subprocess.PIPE, 197*4882a593Smuzhiyun env=ENVIR) 198*4882a593Smuzhiyun 199*4882a593Smuzhiyun try: 200*4882a593Smuzhiyun (rawout, serr) = proc.communicate(timeout=NAMES['TIMEOUT']) 201*4882a593Smuzhiyun if proc.returncode != 0 and len(serr) > 0: 202*4882a593Smuzhiyun foutput = serr.decode("utf-8", errors="ignore") 203*4882a593Smuzhiyun else: 204*4882a593Smuzhiyun foutput = rawout.decode("utf-8", errors="ignore") 205*4882a593Smuzhiyun except subprocess.TimeoutExpired: 206*4882a593Smuzhiyun foutput = "Command \"{}\" timed out\n".format(command) 207*4882a593Smuzhiyun proc.returncode = 255 208*4882a593Smuzhiyun 209*4882a593Smuzhiyun proc.stdout.close() 210*4882a593Smuzhiyun proc.stderr.close() 211*4882a593Smuzhiyun return proc, foutput 212*4882a593Smuzhiyun 213*4882a593Smuzhiyun 214*4882a593Smuzhiyundef prepare_env(args, pm, stage, prefix, cmdlist, output = None): 215*4882a593Smuzhiyun """ 216*4882a593Smuzhiyun Execute the setup/teardown commands for a test case. 217*4882a593Smuzhiyun Optionally terminate test execution if the command fails. 218*4882a593Smuzhiyun """ 219*4882a593Smuzhiyun if args.verbose > 0: 220*4882a593Smuzhiyun print('{}'.format(prefix)) 221*4882a593Smuzhiyun for cmdinfo in cmdlist: 222*4882a593Smuzhiyun if isinstance(cmdinfo, list): 223*4882a593Smuzhiyun exit_codes = cmdinfo[1:] 224*4882a593Smuzhiyun cmd = cmdinfo[0] 225*4882a593Smuzhiyun else: 226*4882a593Smuzhiyun exit_codes = [0] 227*4882a593Smuzhiyun cmd = cmdinfo 228*4882a593Smuzhiyun 229*4882a593Smuzhiyun if not cmd: 230*4882a593Smuzhiyun continue 231*4882a593Smuzhiyun 232*4882a593Smuzhiyun (proc, foutput) = exec_cmd(args, pm, stage, cmd) 233*4882a593Smuzhiyun 234*4882a593Smuzhiyun if proc and (proc.returncode not in exit_codes): 235*4882a593Smuzhiyun print('', file=sys.stderr) 236*4882a593Smuzhiyun print("{} *** Could not execute: \"{}\"".format(prefix, cmd), 237*4882a593Smuzhiyun file=sys.stderr) 238*4882a593Smuzhiyun print("\n{} *** Error message: \"{}\"".format(prefix, foutput), 239*4882a593Smuzhiyun file=sys.stderr) 240*4882a593Smuzhiyun print("returncode {}; expected {}".format(proc.returncode, 241*4882a593Smuzhiyun exit_codes)) 242*4882a593Smuzhiyun print("\n{} *** Aborting test run.".format(prefix), file=sys.stderr) 243*4882a593Smuzhiyun print("\n\n{} *** stdout ***".format(proc.stdout), file=sys.stderr) 244*4882a593Smuzhiyun print("\n\n{} *** stderr ***".format(proc.stderr), file=sys.stderr) 245*4882a593Smuzhiyun raise PluginMgrTestFail( 246*4882a593Smuzhiyun stage, output, 247*4882a593Smuzhiyun '"{}" did not complete successfully'.format(prefix)) 248*4882a593Smuzhiyun 249*4882a593Smuzhiyundef run_one_test(pm, args, index, tidx): 250*4882a593Smuzhiyun global NAMES 251*4882a593Smuzhiyun result = True 252*4882a593Smuzhiyun tresult = "" 253*4882a593Smuzhiyun tap = "" 254*4882a593Smuzhiyun res = TestResult(tidx['id'], tidx['name']) 255*4882a593Smuzhiyun if args.verbose > 0: 256*4882a593Smuzhiyun print("\t====================\n=====> ", end="") 257*4882a593Smuzhiyun print("Test " + tidx["id"] + ": " + tidx["name"]) 258*4882a593Smuzhiyun 259*4882a593Smuzhiyun if 'skip' in tidx: 260*4882a593Smuzhiyun if tidx['skip'] == 'yes': 261*4882a593Smuzhiyun res = TestResult(tidx['id'], tidx['name']) 262*4882a593Smuzhiyun res.set_result(ResultState.skip) 263*4882a593Smuzhiyun res.set_errormsg('Test case designated as skipped.') 264*4882a593Smuzhiyun pm.call_pre_case(tidx, test_skip=True) 265*4882a593Smuzhiyun pm.call_post_execute() 266*4882a593Smuzhiyun return res 267*4882a593Smuzhiyun 268*4882a593Smuzhiyun # populate NAMES with TESTID for this test 269*4882a593Smuzhiyun NAMES['TESTID'] = tidx['id'] 270*4882a593Smuzhiyun 271*4882a593Smuzhiyun pm.call_pre_case(tidx) 272*4882a593Smuzhiyun prepare_env(args, pm, 'setup', "-----> prepare stage", tidx["setup"]) 273*4882a593Smuzhiyun 274*4882a593Smuzhiyun if (args.verbose > 0): 275*4882a593Smuzhiyun print('-----> execute stage') 276*4882a593Smuzhiyun pm.call_pre_execute() 277*4882a593Smuzhiyun (p, procout) = exec_cmd(args, pm, 'execute', tidx["cmdUnderTest"]) 278*4882a593Smuzhiyun if p: 279*4882a593Smuzhiyun exit_code = p.returncode 280*4882a593Smuzhiyun else: 281*4882a593Smuzhiyun exit_code = None 282*4882a593Smuzhiyun 283*4882a593Smuzhiyun pm.call_post_execute() 284*4882a593Smuzhiyun 285*4882a593Smuzhiyun if (exit_code is None or exit_code != int(tidx["expExitCode"])): 286*4882a593Smuzhiyun print("exit: {!r}".format(exit_code)) 287*4882a593Smuzhiyun print("exit: {}".format(int(tidx["expExitCode"]))) 288*4882a593Smuzhiyun #print("exit: {!r} {}".format(exit_code, int(tidx["expExitCode"]))) 289*4882a593Smuzhiyun res.set_result(ResultState.fail) 290*4882a593Smuzhiyun res.set_failmsg('Command exited with {}, expected {}\n{}'.format(exit_code, tidx["expExitCode"], procout)) 291*4882a593Smuzhiyun print(procout) 292*4882a593Smuzhiyun else: 293*4882a593Smuzhiyun if args.verbose > 0: 294*4882a593Smuzhiyun print('-----> verify stage') 295*4882a593Smuzhiyun match_pattern = re.compile( 296*4882a593Smuzhiyun str(tidx["matchPattern"]), re.DOTALL | re.MULTILINE) 297*4882a593Smuzhiyun (p, procout) = exec_cmd(args, pm, 'verify', tidx["verifyCmd"]) 298*4882a593Smuzhiyun if procout: 299*4882a593Smuzhiyun match_index = re.findall(match_pattern, procout) 300*4882a593Smuzhiyun if len(match_index) != int(tidx["matchCount"]): 301*4882a593Smuzhiyun res.set_result(ResultState.fail) 302*4882a593Smuzhiyun res.set_failmsg('Could not match regex pattern. Verify command output:\n{}'.format(procout)) 303*4882a593Smuzhiyun else: 304*4882a593Smuzhiyun res.set_result(ResultState.success) 305*4882a593Smuzhiyun elif int(tidx["matchCount"]) != 0: 306*4882a593Smuzhiyun res.set_result(ResultState.fail) 307*4882a593Smuzhiyun res.set_failmsg('No output generated by verify command.') 308*4882a593Smuzhiyun else: 309*4882a593Smuzhiyun res.set_result(ResultState.success) 310*4882a593Smuzhiyun 311*4882a593Smuzhiyun prepare_env(args, pm, 'teardown', '-----> teardown stage', tidx['teardown'], procout) 312*4882a593Smuzhiyun pm.call_post_case() 313*4882a593Smuzhiyun 314*4882a593Smuzhiyun index += 1 315*4882a593Smuzhiyun 316*4882a593Smuzhiyun # remove TESTID from NAMES 317*4882a593Smuzhiyun del(NAMES['TESTID']) 318*4882a593Smuzhiyun return res 319*4882a593Smuzhiyun 320*4882a593Smuzhiyundef test_runner(pm, args, filtered_tests): 321*4882a593Smuzhiyun """ 322*4882a593Smuzhiyun Driver function for the unit tests. 323*4882a593Smuzhiyun 324*4882a593Smuzhiyun Prints information about the tests being run, executes the setup and 325*4882a593Smuzhiyun teardown commands and the command under test itself. Also determines 326*4882a593Smuzhiyun success/failure based on the information in the test case and generates 327*4882a593Smuzhiyun TAP output accordingly. 328*4882a593Smuzhiyun """ 329*4882a593Smuzhiyun testlist = filtered_tests 330*4882a593Smuzhiyun tcount = len(testlist) 331*4882a593Smuzhiyun index = 1 332*4882a593Smuzhiyun tap = '' 333*4882a593Smuzhiyun badtest = None 334*4882a593Smuzhiyun stage = None 335*4882a593Smuzhiyun emergency_exit = False 336*4882a593Smuzhiyun emergency_exit_message = '' 337*4882a593Smuzhiyun 338*4882a593Smuzhiyun tsr = TestSuiteReport() 339*4882a593Smuzhiyun 340*4882a593Smuzhiyun try: 341*4882a593Smuzhiyun pm.call_pre_suite(tcount, [tidx['id'] for tidx in testlist]) 342*4882a593Smuzhiyun except Exception as ee: 343*4882a593Smuzhiyun ex_type, ex, ex_tb = sys.exc_info() 344*4882a593Smuzhiyun print('Exception {} {} (caught in pre_suite).'. 345*4882a593Smuzhiyun format(ex_type, ex)) 346*4882a593Smuzhiyun traceback.print_tb(ex_tb) 347*4882a593Smuzhiyun emergency_exit_message = 'EMERGENCY EXIT, call_pre_suite failed with exception {} {}\n'.format(ex_type, ex) 348*4882a593Smuzhiyun emergency_exit = True 349*4882a593Smuzhiyun stage = 'pre-SUITE' 350*4882a593Smuzhiyun 351*4882a593Smuzhiyun if emergency_exit: 352*4882a593Smuzhiyun pm.call_post_suite(index) 353*4882a593Smuzhiyun return emergency_exit_message 354*4882a593Smuzhiyun if args.verbose > 1: 355*4882a593Smuzhiyun print('give test rig 2 seconds to stabilize') 356*4882a593Smuzhiyun time.sleep(2) 357*4882a593Smuzhiyun for tidx in testlist: 358*4882a593Smuzhiyun if "flower" in tidx["category"] and args.device == None: 359*4882a593Smuzhiyun errmsg = "Tests using the DEV2 variable must define the name of a " 360*4882a593Smuzhiyun errmsg += "physical NIC with the -d option when running tdc.\n" 361*4882a593Smuzhiyun errmsg += "Test has been skipped." 362*4882a593Smuzhiyun if args.verbose > 1: 363*4882a593Smuzhiyun print(errmsg) 364*4882a593Smuzhiyun res = TestResult(tidx['id'], tidx['name']) 365*4882a593Smuzhiyun res.set_result(ResultState.skip) 366*4882a593Smuzhiyun res.set_errormsg(errmsg) 367*4882a593Smuzhiyun tsr.add_resultdata(res) 368*4882a593Smuzhiyun continue 369*4882a593Smuzhiyun try: 370*4882a593Smuzhiyun badtest = tidx # in case it goes bad 371*4882a593Smuzhiyun res = run_one_test(pm, args, index, tidx) 372*4882a593Smuzhiyun tsr.add_resultdata(res) 373*4882a593Smuzhiyun except PluginMgrTestFail as pmtf: 374*4882a593Smuzhiyun ex_type, ex, ex_tb = sys.exc_info() 375*4882a593Smuzhiyun stage = pmtf.stage 376*4882a593Smuzhiyun message = pmtf.message 377*4882a593Smuzhiyun output = pmtf.output 378*4882a593Smuzhiyun res = TestResult(tidx['id'], tidx['name']) 379*4882a593Smuzhiyun res.set_result(ResultState.skip) 380*4882a593Smuzhiyun res.set_errormsg(pmtf.message) 381*4882a593Smuzhiyun res.set_failmsg(pmtf.output) 382*4882a593Smuzhiyun tsr.add_resultdata(res) 383*4882a593Smuzhiyun index += 1 384*4882a593Smuzhiyun print(message) 385*4882a593Smuzhiyun print('Exception {} {} (caught in test_runner, running test {} {} {} stage {})'. 386*4882a593Smuzhiyun format(ex_type, ex, index, tidx['id'], tidx['name'], stage)) 387*4882a593Smuzhiyun print('---------------') 388*4882a593Smuzhiyun print('traceback') 389*4882a593Smuzhiyun traceback.print_tb(ex_tb) 390*4882a593Smuzhiyun print('---------------') 391*4882a593Smuzhiyun if stage == 'teardown': 392*4882a593Smuzhiyun print('accumulated output for this test:') 393*4882a593Smuzhiyun if pmtf.output: 394*4882a593Smuzhiyun print(pmtf.output) 395*4882a593Smuzhiyun print('---------------') 396*4882a593Smuzhiyun break 397*4882a593Smuzhiyun index += 1 398*4882a593Smuzhiyun 399*4882a593Smuzhiyun # if we failed in setup or teardown, 400*4882a593Smuzhiyun # fill in the remaining tests with ok-skipped 401*4882a593Smuzhiyun count = index 402*4882a593Smuzhiyun 403*4882a593Smuzhiyun if tcount + 1 != count: 404*4882a593Smuzhiyun for tidx in testlist[count - 1:]: 405*4882a593Smuzhiyun res = TestResult(tidx['id'], tidx['name']) 406*4882a593Smuzhiyun res.set_result(ResultState.skip) 407*4882a593Smuzhiyun msg = 'skipped - previous {} failed {} {}'.format(stage, 408*4882a593Smuzhiyun index, badtest.get('id', '--Unknown--')) 409*4882a593Smuzhiyun res.set_errormsg(msg) 410*4882a593Smuzhiyun tsr.add_resultdata(res) 411*4882a593Smuzhiyun count += 1 412*4882a593Smuzhiyun 413*4882a593Smuzhiyun if args.pause: 414*4882a593Smuzhiyun print('Want to pause\nPress enter to continue ...') 415*4882a593Smuzhiyun if input(sys.stdin): 416*4882a593Smuzhiyun print('got something on stdin') 417*4882a593Smuzhiyun 418*4882a593Smuzhiyun pm.call_post_suite(index) 419*4882a593Smuzhiyun 420*4882a593Smuzhiyun return tsr 421*4882a593Smuzhiyun 422*4882a593Smuzhiyundef has_blank_ids(idlist): 423*4882a593Smuzhiyun """ 424*4882a593Smuzhiyun Search the list for empty ID fields and return true/false accordingly. 425*4882a593Smuzhiyun """ 426*4882a593Smuzhiyun return not(all(k for k in idlist)) 427*4882a593Smuzhiyun 428*4882a593Smuzhiyun 429*4882a593Smuzhiyundef load_from_file(filename): 430*4882a593Smuzhiyun """ 431*4882a593Smuzhiyun Open the JSON file containing the test cases and return them 432*4882a593Smuzhiyun as list of ordered dictionary objects. 433*4882a593Smuzhiyun """ 434*4882a593Smuzhiyun try: 435*4882a593Smuzhiyun with open(filename) as test_data: 436*4882a593Smuzhiyun testlist = json.load(test_data, object_pairs_hook=OrderedDict) 437*4882a593Smuzhiyun except json.JSONDecodeError as jde: 438*4882a593Smuzhiyun print('IGNORING test case file {}\n\tBECAUSE: {}'.format(filename, jde)) 439*4882a593Smuzhiyun testlist = list() 440*4882a593Smuzhiyun else: 441*4882a593Smuzhiyun idlist = get_id_list(testlist) 442*4882a593Smuzhiyun if (has_blank_ids(idlist)): 443*4882a593Smuzhiyun for k in testlist: 444*4882a593Smuzhiyun k['filename'] = filename 445*4882a593Smuzhiyun return testlist 446*4882a593Smuzhiyun 447*4882a593Smuzhiyun 448*4882a593Smuzhiyundef args_parse(): 449*4882a593Smuzhiyun """ 450*4882a593Smuzhiyun Create the argument parser. 451*4882a593Smuzhiyun """ 452*4882a593Smuzhiyun parser = argparse.ArgumentParser(description='Linux TC unit tests') 453*4882a593Smuzhiyun return parser 454*4882a593Smuzhiyun 455*4882a593Smuzhiyun 456*4882a593Smuzhiyundef set_args(parser): 457*4882a593Smuzhiyun """ 458*4882a593Smuzhiyun Set the command line arguments for tdc. 459*4882a593Smuzhiyun """ 460*4882a593Smuzhiyun parser.add_argument( 461*4882a593Smuzhiyun '--outfile', type=str, 462*4882a593Smuzhiyun help='Path to the file in which results should be saved. ' + 463*4882a593Smuzhiyun 'Default target is the current directory.') 464*4882a593Smuzhiyun parser.add_argument( 465*4882a593Smuzhiyun '-p', '--path', type=str, 466*4882a593Smuzhiyun help='The full path to the tc executable to use') 467*4882a593Smuzhiyun sg = parser.add_argument_group( 468*4882a593Smuzhiyun 'selection', 'select which test cases: ' + 469*4882a593Smuzhiyun 'files plus directories; filtered by categories plus testids') 470*4882a593Smuzhiyun ag = parser.add_argument_group( 471*4882a593Smuzhiyun 'action', 'select action to perform on selected test cases') 472*4882a593Smuzhiyun 473*4882a593Smuzhiyun sg.add_argument( 474*4882a593Smuzhiyun '-D', '--directory', nargs='+', metavar='DIR', 475*4882a593Smuzhiyun help='Collect tests from the specified directory(ies) ' + 476*4882a593Smuzhiyun '(default [tc-tests])') 477*4882a593Smuzhiyun sg.add_argument( 478*4882a593Smuzhiyun '-f', '--file', nargs='+', metavar='FILE', 479*4882a593Smuzhiyun help='Run tests from the specified file(s)') 480*4882a593Smuzhiyun sg.add_argument( 481*4882a593Smuzhiyun '-c', '--category', nargs='*', metavar='CATG', default=['+c'], 482*4882a593Smuzhiyun help='Run tests only from the specified category/ies, ' + 483*4882a593Smuzhiyun 'or if no category/ies is/are specified, list known categories.') 484*4882a593Smuzhiyun sg.add_argument( 485*4882a593Smuzhiyun '-e', '--execute', nargs='+', metavar='ID', 486*4882a593Smuzhiyun help='Execute the specified test cases with specified IDs') 487*4882a593Smuzhiyun ag.add_argument( 488*4882a593Smuzhiyun '-l', '--list', action='store_true', 489*4882a593Smuzhiyun help='List all test cases, or those only within the specified category') 490*4882a593Smuzhiyun ag.add_argument( 491*4882a593Smuzhiyun '-s', '--show', action='store_true', dest='showID', 492*4882a593Smuzhiyun help='Display the selected test cases') 493*4882a593Smuzhiyun ag.add_argument( 494*4882a593Smuzhiyun '-i', '--id', action='store_true', dest='gen_id', 495*4882a593Smuzhiyun help='Generate ID numbers for new test cases') 496*4882a593Smuzhiyun parser.add_argument( 497*4882a593Smuzhiyun '-v', '--verbose', action='count', default=0, 498*4882a593Smuzhiyun help='Show the commands that are being run') 499*4882a593Smuzhiyun parser.add_argument( 500*4882a593Smuzhiyun '--format', default='tap', const='tap', nargs='?', 501*4882a593Smuzhiyun choices=['none', 'xunit', 'tap'], 502*4882a593Smuzhiyun help='Specify the format for test results. (Default: TAP)') 503*4882a593Smuzhiyun parser.add_argument('-d', '--device', 504*4882a593Smuzhiyun help='Execute test cases that use a physical device, ' + 505*4882a593Smuzhiyun 'where DEVICE is its name. (If not defined, tests ' + 506*4882a593Smuzhiyun 'that require a physical device will be skipped)') 507*4882a593Smuzhiyun parser.add_argument( 508*4882a593Smuzhiyun '-P', '--pause', action='store_true', 509*4882a593Smuzhiyun help='Pause execution just before post-suite stage') 510*4882a593Smuzhiyun return parser 511*4882a593Smuzhiyun 512*4882a593Smuzhiyun 513*4882a593Smuzhiyundef check_default_settings(args, remaining, pm): 514*4882a593Smuzhiyun """ 515*4882a593Smuzhiyun Process any arguments overriding the default settings, 516*4882a593Smuzhiyun and ensure the settings are correct. 517*4882a593Smuzhiyun """ 518*4882a593Smuzhiyun # Allow for overriding specific settings 519*4882a593Smuzhiyun global NAMES 520*4882a593Smuzhiyun 521*4882a593Smuzhiyun if args.path != None: 522*4882a593Smuzhiyun NAMES['TC'] = args.path 523*4882a593Smuzhiyun if args.device != None: 524*4882a593Smuzhiyun NAMES['DEV2'] = args.device 525*4882a593Smuzhiyun if 'TIMEOUT' not in NAMES: 526*4882a593Smuzhiyun NAMES['TIMEOUT'] = None 527*4882a593Smuzhiyun if not os.path.isfile(NAMES['TC']): 528*4882a593Smuzhiyun print("The specified tc path " + NAMES['TC'] + " does not exist.") 529*4882a593Smuzhiyun exit(1) 530*4882a593Smuzhiyun 531*4882a593Smuzhiyun pm.call_check_args(args, remaining) 532*4882a593Smuzhiyun 533*4882a593Smuzhiyun 534*4882a593Smuzhiyundef get_id_list(alltests): 535*4882a593Smuzhiyun """ 536*4882a593Smuzhiyun Generate a list of all IDs in the test cases. 537*4882a593Smuzhiyun """ 538*4882a593Smuzhiyun return [x["id"] for x in alltests] 539*4882a593Smuzhiyun 540*4882a593Smuzhiyun 541*4882a593Smuzhiyundef check_case_id(alltests): 542*4882a593Smuzhiyun """ 543*4882a593Smuzhiyun Check for duplicate test case IDs. 544*4882a593Smuzhiyun """ 545*4882a593Smuzhiyun idl = get_id_list(alltests) 546*4882a593Smuzhiyun return [x for x in idl if idl.count(x) > 1] 547*4882a593Smuzhiyun 548*4882a593Smuzhiyun 549*4882a593Smuzhiyundef does_id_exist(alltests, newid): 550*4882a593Smuzhiyun """ 551*4882a593Smuzhiyun Check if a given ID already exists in the list of test cases. 552*4882a593Smuzhiyun """ 553*4882a593Smuzhiyun idl = get_id_list(alltests) 554*4882a593Smuzhiyun return (any(newid == x for x in idl)) 555*4882a593Smuzhiyun 556*4882a593Smuzhiyun 557*4882a593Smuzhiyundef generate_case_ids(alltests): 558*4882a593Smuzhiyun """ 559*4882a593Smuzhiyun If a test case has a blank ID field, generate a random hex ID for it 560*4882a593Smuzhiyun and then write the test cases back to disk. 561*4882a593Smuzhiyun """ 562*4882a593Smuzhiyun import random 563*4882a593Smuzhiyun for c in alltests: 564*4882a593Smuzhiyun if (c["id"] == ""): 565*4882a593Smuzhiyun while True: 566*4882a593Smuzhiyun newid = str('{:04x}'.format(random.randrange(16**4))) 567*4882a593Smuzhiyun if (does_id_exist(alltests, newid)): 568*4882a593Smuzhiyun continue 569*4882a593Smuzhiyun else: 570*4882a593Smuzhiyun c['id'] = newid 571*4882a593Smuzhiyun break 572*4882a593Smuzhiyun 573*4882a593Smuzhiyun ufilename = [] 574*4882a593Smuzhiyun for c in alltests: 575*4882a593Smuzhiyun if ('filename' in c): 576*4882a593Smuzhiyun ufilename.append(c['filename']) 577*4882a593Smuzhiyun ufilename = get_unique_item(ufilename) 578*4882a593Smuzhiyun for f in ufilename: 579*4882a593Smuzhiyun testlist = [] 580*4882a593Smuzhiyun for t in alltests: 581*4882a593Smuzhiyun if 'filename' in t: 582*4882a593Smuzhiyun if t['filename'] == f: 583*4882a593Smuzhiyun del t['filename'] 584*4882a593Smuzhiyun testlist.append(t) 585*4882a593Smuzhiyun outfile = open(f, "w") 586*4882a593Smuzhiyun json.dump(testlist, outfile, indent=4) 587*4882a593Smuzhiyun outfile.write("\n") 588*4882a593Smuzhiyun outfile.close() 589*4882a593Smuzhiyun 590*4882a593Smuzhiyundef filter_tests_by_id(args, testlist): 591*4882a593Smuzhiyun ''' 592*4882a593Smuzhiyun Remove tests from testlist that are not in the named id list. 593*4882a593Smuzhiyun If id list is empty, return empty list. 594*4882a593Smuzhiyun ''' 595*4882a593Smuzhiyun newlist = list() 596*4882a593Smuzhiyun if testlist and args.execute: 597*4882a593Smuzhiyun target_ids = args.execute 598*4882a593Smuzhiyun 599*4882a593Smuzhiyun if isinstance(target_ids, list) and (len(target_ids) > 0): 600*4882a593Smuzhiyun newlist = list(filter(lambda x: x['id'] in target_ids, testlist)) 601*4882a593Smuzhiyun return newlist 602*4882a593Smuzhiyun 603*4882a593Smuzhiyundef filter_tests_by_category(args, testlist): 604*4882a593Smuzhiyun ''' 605*4882a593Smuzhiyun Remove tests from testlist that are not in a named category. 606*4882a593Smuzhiyun ''' 607*4882a593Smuzhiyun answer = list() 608*4882a593Smuzhiyun if args.category and testlist: 609*4882a593Smuzhiyun test_ids = list() 610*4882a593Smuzhiyun for catg in set(args.category): 611*4882a593Smuzhiyun if catg == '+c': 612*4882a593Smuzhiyun continue 613*4882a593Smuzhiyun print('considering category {}'.format(catg)) 614*4882a593Smuzhiyun for tc in testlist: 615*4882a593Smuzhiyun if catg in tc['category'] and tc['id'] not in test_ids: 616*4882a593Smuzhiyun answer.append(tc) 617*4882a593Smuzhiyun test_ids.append(tc['id']) 618*4882a593Smuzhiyun 619*4882a593Smuzhiyun return answer 620*4882a593Smuzhiyun 621*4882a593Smuzhiyun 622*4882a593Smuzhiyundef get_test_cases(args): 623*4882a593Smuzhiyun """ 624*4882a593Smuzhiyun If a test case file is specified, retrieve tests from that file. 625*4882a593Smuzhiyun Otherwise, glob for all json files in subdirectories and load from 626*4882a593Smuzhiyun each one. 627*4882a593Smuzhiyun Also, if requested, filter by category, and add tests matching 628*4882a593Smuzhiyun certain ids. 629*4882a593Smuzhiyun """ 630*4882a593Smuzhiyun import fnmatch 631*4882a593Smuzhiyun 632*4882a593Smuzhiyun flist = [] 633*4882a593Smuzhiyun testdirs = ['tc-tests'] 634*4882a593Smuzhiyun 635*4882a593Smuzhiyun if args.file: 636*4882a593Smuzhiyun # at least one file was specified - remove the default directory 637*4882a593Smuzhiyun testdirs = [] 638*4882a593Smuzhiyun 639*4882a593Smuzhiyun for ff in args.file: 640*4882a593Smuzhiyun if not os.path.isfile(ff): 641*4882a593Smuzhiyun print("IGNORING file " + ff + "\n\tBECAUSE does not exist.") 642*4882a593Smuzhiyun else: 643*4882a593Smuzhiyun flist.append(os.path.abspath(ff)) 644*4882a593Smuzhiyun 645*4882a593Smuzhiyun if args.directory: 646*4882a593Smuzhiyun testdirs = args.directory 647*4882a593Smuzhiyun 648*4882a593Smuzhiyun for testdir in testdirs: 649*4882a593Smuzhiyun for root, dirnames, filenames in os.walk(testdir): 650*4882a593Smuzhiyun for filename in fnmatch.filter(filenames, '*.json'): 651*4882a593Smuzhiyun candidate = os.path.abspath(os.path.join(root, filename)) 652*4882a593Smuzhiyun if candidate not in testdirs: 653*4882a593Smuzhiyun flist.append(candidate) 654*4882a593Smuzhiyun 655*4882a593Smuzhiyun alltestcases = list() 656*4882a593Smuzhiyun for casefile in flist: 657*4882a593Smuzhiyun alltestcases = alltestcases + (load_from_file(casefile)) 658*4882a593Smuzhiyun 659*4882a593Smuzhiyun allcatlist = get_test_categories(alltestcases) 660*4882a593Smuzhiyun allidlist = get_id_list(alltestcases) 661*4882a593Smuzhiyun 662*4882a593Smuzhiyun testcases_by_cats = get_categorized_testlist(alltestcases, allcatlist) 663*4882a593Smuzhiyun idtestcases = filter_tests_by_id(args, alltestcases) 664*4882a593Smuzhiyun cattestcases = filter_tests_by_category(args, alltestcases) 665*4882a593Smuzhiyun 666*4882a593Smuzhiyun cat_ids = [x['id'] for x in cattestcases] 667*4882a593Smuzhiyun if args.execute: 668*4882a593Smuzhiyun if args.category: 669*4882a593Smuzhiyun alltestcases = cattestcases + [x for x in idtestcases if x['id'] not in cat_ids] 670*4882a593Smuzhiyun else: 671*4882a593Smuzhiyun alltestcases = idtestcases 672*4882a593Smuzhiyun else: 673*4882a593Smuzhiyun if cat_ids: 674*4882a593Smuzhiyun alltestcases = cattestcases 675*4882a593Smuzhiyun else: 676*4882a593Smuzhiyun # just accept the existing value of alltestcases, 677*4882a593Smuzhiyun # which has been filtered by file/directory 678*4882a593Smuzhiyun pass 679*4882a593Smuzhiyun 680*4882a593Smuzhiyun return allcatlist, allidlist, testcases_by_cats, alltestcases 681*4882a593Smuzhiyun 682*4882a593Smuzhiyun 683*4882a593Smuzhiyundef set_operation_mode(pm, parser, args, remaining): 684*4882a593Smuzhiyun """ 685*4882a593Smuzhiyun Load the test case data and process remaining arguments to determine 686*4882a593Smuzhiyun what the script should do for this run, and call the appropriate 687*4882a593Smuzhiyun function. 688*4882a593Smuzhiyun """ 689*4882a593Smuzhiyun ucat, idlist, testcases, alltests = get_test_cases(args) 690*4882a593Smuzhiyun 691*4882a593Smuzhiyun if args.gen_id: 692*4882a593Smuzhiyun if (has_blank_ids(idlist)): 693*4882a593Smuzhiyun alltests = generate_case_ids(alltests) 694*4882a593Smuzhiyun else: 695*4882a593Smuzhiyun print("No empty ID fields found in test files.") 696*4882a593Smuzhiyun exit(0) 697*4882a593Smuzhiyun 698*4882a593Smuzhiyun duplicate_ids = check_case_id(alltests) 699*4882a593Smuzhiyun if (len(duplicate_ids) > 0): 700*4882a593Smuzhiyun print("The following test case IDs are not unique:") 701*4882a593Smuzhiyun print(str(set(duplicate_ids))) 702*4882a593Smuzhiyun print("Please correct them before continuing.") 703*4882a593Smuzhiyun exit(1) 704*4882a593Smuzhiyun 705*4882a593Smuzhiyun if args.showID: 706*4882a593Smuzhiyun for atest in alltests: 707*4882a593Smuzhiyun print_test_case(atest) 708*4882a593Smuzhiyun exit(0) 709*4882a593Smuzhiyun 710*4882a593Smuzhiyun if isinstance(args.category, list) and (len(args.category) == 0): 711*4882a593Smuzhiyun print("Available categories:") 712*4882a593Smuzhiyun print_sll(ucat) 713*4882a593Smuzhiyun exit(0) 714*4882a593Smuzhiyun 715*4882a593Smuzhiyun if args.list: 716*4882a593Smuzhiyun list_test_cases(alltests) 717*4882a593Smuzhiyun exit(0) 718*4882a593Smuzhiyun 719*4882a593Smuzhiyun if len(alltests): 720*4882a593Smuzhiyun req_plugins = pm.get_required_plugins(alltests) 721*4882a593Smuzhiyun try: 722*4882a593Smuzhiyun args = pm.load_required_plugins(req_plugins, parser, args, remaining) 723*4882a593Smuzhiyun except PluginDependencyException as pde: 724*4882a593Smuzhiyun print('The following plugins were not found:') 725*4882a593Smuzhiyun print('{}'.format(pde.missing_pg)) 726*4882a593Smuzhiyun catresults = test_runner(pm, args, alltests) 727*4882a593Smuzhiyun if args.format == 'none': 728*4882a593Smuzhiyun print('Test results output suppression requested\n') 729*4882a593Smuzhiyun else: 730*4882a593Smuzhiyun print('\nAll test results: \n') 731*4882a593Smuzhiyun if args.format == 'xunit': 732*4882a593Smuzhiyun suffix = 'xml' 733*4882a593Smuzhiyun res = catresults.format_xunit() 734*4882a593Smuzhiyun elif args.format == 'tap': 735*4882a593Smuzhiyun suffix = 'tap' 736*4882a593Smuzhiyun res = catresults.format_tap() 737*4882a593Smuzhiyun print(res) 738*4882a593Smuzhiyun print('\n\n') 739*4882a593Smuzhiyun if not args.outfile: 740*4882a593Smuzhiyun fname = 'test-results.{}'.format(suffix) 741*4882a593Smuzhiyun else: 742*4882a593Smuzhiyun fname = args.outfile 743*4882a593Smuzhiyun with open(fname, 'w') as fh: 744*4882a593Smuzhiyun fh.write(res) 745*4882a593Smuzhiyun fh.close() 746*4882a593Smuzhiyun if os.getenv('SUDO_UID') is not None: 747*4882a593Smuzhiyun os.chown(fname, uid=int(os.getenv('SUDO_UID')), 748*4882a593Smuzhiyun gid=int(os.getenv('SUDO_GID'))) 749*4882a593Smuzhiyun else: 750*4882a593Smuzhiyun print('No tests found\n') 751*4882a593Smuzhiyun 752*4882a593Smuzhiyundef main(): 753*4882a593Smuzhiyun """ 754*4882a593Smuzhiyun Start of execution; set up argument parser and get the arguments, 755*4882a593Smuzhiyun and start operations. 756*4882a593Smuzhiyun """ 757*4882a593Smuzhiyun parser = args_parse() 758*4882a593Smuzhiyun parser = set_args(parser) 759*4882a593Smuzhiyun pm = PluginMgr(parser) 760*4882a593Smuzhiyun parser = pm.call_add_args(parser) 761*4882a593Smuzhiyun (args, remaining) = parser.parse_known_args() 762*4882a593Smuzhiyun args.NAMES = NAMES 763*4882a593Smuzhiyun pm.set_args(args) 764*4882a593Smuzhiyun check_default_settings(args, remaining, pm) 765*4882a593Smuzhiyun if args.verbose > 2: 766*4882a593Smuzhiyun print('args is {}'.format(args)) 767*4882a593Smuzhiyun 768*4882a593Smuzhiyun set_operation_mode(pm, parser, args, remaining) 769*4882a593Smuzhiyun 770*4882a593Smuzhiyun exit(0) 771*4882a593Smuzhiyun 772*4882a593Smuzhiyun 773*4882a593Smuzhiyunif __name__ == "__main__": 774*4882a593Smuzhiyun main() 775