2 # SPDX-License-Identifier: GPL-2.0
5 tdc.py - Linux tc (Traffic Control) unit test driver
19 from collections import OrderedDict
20 from string import Template
22 from tdc_config import *
23 from tdc_helper import *
28 class PluginMgrTestFail(Exception):
29 def __init__(self, stage, output, message):
32 self.message = message
35 def __init__(self, argparser):
38 self.plugin_instances = []
40 self.argparser = argparser
42 # TODO, put plugins in order
43 plugindir = os.getenv('TDC_PLUGIN_DIR', './plugins')
44 for dirpath, dirnames, filenames in os.walk(plugindir):
46 if (fn.endswith('.py') and
47 not fn == '__init__.py' and
48 not fn.startswith('#') and
49 not fn.startswith('.#')):
51 foo = importlib.import_module('plugins.' + mn)
52 self.plugins[mn] = foo
53 self.plugin_instances.append(foo.SubPlugin())
55 def call_pre_suite(self, testcount, testidlist):
56 for pgn_inst in self.plugin_instances:
57 pgn_inst.pre_suite(testcount, testidlist)
59 def call_post_suite(self, index):
60 for pgn_inst in reversed(self.plugin_instances):
61 pgn_inst.post_suite(index)
63 def call_pre_case(self, test_ordinal, testid):
64 for pgn_inst in self.plugin_instances:
66 pgn_inst.pre_case(test_ordinal, testid)
67 except Exception as ee:
68 print('exception {} in call to pre_case for {} plugin'.
69 format(ee, pgn_inst.__class__))
70 print('test_ordinal is {}'.format(test_ordinal))
71 print('testid is {}'.format(testid))
74 def call_post_case(self):
75 for pgn_inst in reversed(self.plugin_instances):
78 def call_pre_execute(self):
79 for pgn_inst in self.plugin_instances:
80 pgn_inst.pre_execute()
82 def call_post_execute(self):
83 for pgn_inst in reversed(self.plugin_instances):
84 pgn_inst.post_execute()
86 def call_add_args(self, parser):
87 for pgn_inst in self.plugin_instances:
88 parser = pgn_inst.add_args(parser)
91 def call_check_args(self, args, remaining):
92 for pgn_inst in self.plugin_instances:
93 pgn_inst.check_args(args, remaining)
95 def call_adjust_command(self, stage, command):
96 for pgn_inst in self.plugin_instances:
97 command = pgn_inst.adjust_command(stage, command)
101 def _make_argparser(args):
102 self.argparser = argparse.ArgumentParser(
103 description='Linux TC unit tests')
106 def replace_keywords(cmd):
108 For a given executable command, substitute any known
109 variables contained within NAMES with the correct values
112 subcmd = tcmd.safe_substitute(NAMES)
116 def exec_cmd(args, pm, stage, command):
118 Perform any required modifications on an executable command, then run
119 it in a subprocess and return the results.
121 if len(command.strip()) == 0:
124 command = replace_keywords(command)
126 command = pm.call_adjust_command(stage, command)
128 print('command "{}"'.format(command))
129 proc = subprocess.Popen(command,
131 stdout=subprocess.PIPE,
132 stderr=subprocess.PIPE,
134 (rawout, serr) = proc.communicate()
136 if proc.returncode != 0 and len(serr) > 0:
137 foutput = serr.decode("utf-8")
139 foutput = rawout.decode("utf-8")
146 def prepare_env(args, pm, stage, prefix, cmdlist, output = None):
148 Execute the setup/teardown commands for a test case.
149 Optionally terminate test execution if the command fails.
152 print('{}'.format(prefix))
153 for cmdinfo in cmdlist:
154 if isinstance(cmdinfo, list):
155 exit_codes = cmdinfo[1:]
164 (proc, foutput) = exec_cmd(args, pm, stage, cmd)
166 if proc and (proc.returncode not in exit_codes):
167 print('', file=sys.stderr)
168 print("{} *** Could not execute: \"{}\"".format(prefix, cmd),
170 print("\n{} *** Error message: \"{}\"".format(prefix, foutput),
172 print("\n{} *** Aborting test run.".format(prefix), file=sys.stderr)
173 print("\n\n{} *** stdout ***".format(proc.stdout), file=sys.stderr)
174 print("\n\n{} *** stderr ***".format(proc.stderr), file=sys.stderr)
175 raise PluginMgrTestFail(
177 '"{}" did not complete successfully'.format(prefix))
179 def run_one_test(pm, args, index, tidx):
185 print("\t====================\n=====> ", end="")
186 print("Test " + tidx["id"] + ": " + tidx["name"])
188 # populate NAMES with TESTID for this test
189 NAMES['TESTID'] = tidx['id']
191 pm.call_pre_case(index, tidx['id'])
192 prepare_env(args, pm, 'setup', "-----> prepare stage", tidx["setup"])
194 if (args.verbose > 0):
195 print('-----> execute stage')
196 pm.call_pre_execute()
197 (p, procout) = exec_cmd(args, pm, 'execute', tidx["cmdUnderTest"])
198 exit_code = p.returncode
199 pm.call_post_execute()
201 if (exit_code != int(tidx["expExitCode"])):
203 print("exit:", exit_code, int(tidx["expExitCode"]))
207 print('-----> verify stage')
208 match_pattern = re.compile(
209 str(tidx["matchPattern"]), re.DOTALL | re.MULTILINE)
210 (p, procout) = exec_cmd(args, pm, 'verify', tidx["verifyCmd"])
212 match_index = re.findall(match_pattern, procout)
213 if len(match_index) != int(tidx["matchCount"]):
215 elif int(tidx["matchCount"]) != 0:
220 tresult += 'ok {} - {} # {}\n'.format(str(index), tidx['id'], tidx['name'])
227 tap += 'No output!\n'
229 prepare_env(args, pm, 'teardown', '-----> teardown stage', tidx['teardown'], procout)
234 # remove TESTID from NAMES
238 def test_runner(pm, args, filtered_tests):
240 Driver function for the unit tests.
242 Prints information about the tests being run, executes the setup and
243 teardown commands and the command under test itself. Also determines
244 success/failure based on the information in the test case and generates
245 TAP output accordingly.
247 testlist = filtered_tests
248 tcount = len(testlist)
253 emergency_exit = False
254 emergency_exit_message = ''
258 tap = 'notap requested: omitting test plan\n'
260 tap = str(index) + ".." + str(tcount) + "\n"
262 pm.call_pre_suite(tcount, [tidx['id'] for tidx in testlist])
263 except Exception as ee:
264 ex_type, ex, ex_tb = sys.exc_info()
265 print('Exception {} {} (caught in pre_suite).'.
267 # when the extra print statements are uncommented,
268 # the traceback does not appear between them
269 # (it appears way earlier in the tdc.py output)
270 # so don't bother ...
271 # print('--------------------(')
273 traceback.print_tb(ex_tb)
274 # print('--------------------)')
275 emergency_exit_message = 'EMERGENCY EXIT, call_pre_suite failed with exception {} {}\n'.format(ex_type, ex)
276 emergency_exit = True
280 pm.call_post_suite(index)
281 return emergency_exit_message
283 print('give test rig 2 seconds to stabilize')
285 for tidx in testlist:
286 if "flower" in tidx["category"] and args.device == None:
288 print('Not executing test {} {} because DEV2 not defined'.
289 format(tidx['id'], tidx['name']))
292 badtest = tidx # in case it goes bad
293 tap += run_one_test(pm, args, index, tidx)
294 except PluginMgrTestFail as pmtf:
295 ex_type, ex, ex_tb = sys.exc_info()
297 message = pmtf.message
300 print('Exception {} {} (caught in test_runner, running test {} {} {} stage {})'.
301 format(ex_type, ex, index, tidx['id'], tidx['name'], stage))
302 print('---------------')
304 traceback.print_tb(ex_tb)
305 print('---------------')
306 if stage == 'teardown':
307 print('accumulated output for this test:')
310 print('---------------')
314 # if we failed in setup or teardown,
315 # fill in the remaining tests with ok-skipped
318 tap += 'about to flush the tap output if tests need to be skipped\n'
319 if tcount + 1 != index:
320 for tidx in testlist[index - 1:]:
321 msg = 'skipped - previous {} failed'.format(stage)
322 tap += 'ok {} - {} # {} {} {}\n'.format(
323 count, tidx['id'], msg, index, badtest.get('id', '--Unknown--'))
326 tap += 'done flushing skipped test tap output\n'
329 print('Want to pause\nPress enter to continue ...')
331 print('got something on stdin')
333 pm.call_post_suite(index)
337 def has_blank_ids(idlist):
339 Search the list for empty ID fields and return true/false accordingly.
341 return not(all(k for k in idlist))
344 def load_from_file(filename):
346 Open the JSON file containing the test cases and return them
347 as list of ordered dictionary objects.
350 with open(filename) as test_data:
351 testlist = json.load(test_data, object_pairs_hook=OrderedDict)
352 except json.JSONDecodeError as jde:
353 print('IGNORING test case file {}\n\tBECAUSE: {}'.format(filename, jde))
356 idlist = get_id_list(testlist)
357 if (has_blank_ids(idlist)):
359 k['filename'] = filename
365 Create the argument parser.
367 parser = argparse.ArgumentParser(description='Linux TC unit tests')
371 def set_args(parser):
373 Set the command line arguments for tdc.
376 '-p', '--path', type=str,
377 help='The full path to the tc executable to use')
378 sg = parser.add_argument_group(
379 'selection', 'select which test cases: ' +
380 'files plus directories; filtered by categories plus testids')
381 ag = parser.add_argument_group(
382 'action', 'select action to perform on selected test cases')
385 '-D', '--directory', nargs='+', metavar='DIR',
386 help='Collect tests from the specified directory(ies) ' +
387 '(default [tc-tests])')
389 '-f', '--file', nargs='+', metavar='FILE',
390 help='Run tests from the specified file(s)')
392 '-c', '--category', nargs='*', metavar='CATG', default=['+c'],
393 help='Run tests only from the specified category/ies, ' +
394 'or if no category/ies is/are specified, list known categories.')
396 '-e', '--execute', nargs='+', metavar='ID',
397 help='Execute the specified test cases with specified IDs')
399 '-l', '--list', action='store_true',
400 help='List all test cases, or those only within the specified category')
402 '-s', '--show', action='store_true', dest='showID',
403 help='Display the selected test cases')
405 '-i', '--id', action='store_true', dest='gen_id',
406 help='Generate ID numbers for new test cases')
408 '-v', '--verbose', action='count', default=0,
409 help='Show the commands that are being run')
411 '-N', '--notap', action='store_true',
412 help='Suppress tap results for command under test')
413 parser.add_argument('-d', '--device',
414 help='Execute the test case in flower category')
416 '-P', '--pause', action='store_true',
417 help='Pause execution just before post-suite stage')
421 def check_default_settings(args, remaining, pm):
423 Process any arguments overriding the default settings,
424 and ensure the settings are correct.
426 # Allow for overriding specific settings
429 if args.path != None:
430 NAMES['TC'] = args.path
431 if args.device != None:
432 NAMES['DEV2'] = args.device
433 if not os.path.isfile(NAMES['TC']):
434 print("The specified tc path " + NAMES['TC'] + " does not exist.")
437 pm.call_check_args(args, remaining)
440 def get_id_list(alltests):
442 Generate a list of all IDs in the test cases.
444 return [x["id"] for x in alltests]
447 def check_case_id(alltests):
449 Check for duplicate test case IDs.
451 idl = get_id_list(alltests)
452 return [x for x in idl if idl.count(x) > 1]
455 def does_id_exist(alltests, newid):
457 Check if a given ID already exists in the list of test cases.
459 idl = get_id_list(alltests)
460 return (any(newid == x for x in idl))
463 def generate_case_ids(alltests):
465 If a test case has a blank ID field, generate a random hex ID for it
466 and then write the test cases back to disk.
472 newid = str('{:04x}'.format(random.randrange(16**4)))
473 if (does_id_exist(alltests, newid)):
481 if ('filename' in c):
482 ufilename.append(c['filename'])
483 ufilename = get_unique_item(ufilename)
488 if t['filename'] == f:
491 outfile = open(f, "w")
492 json.dump(testlist, outfile, indent=4)
496 def filter_tests_by_id(args, testlist):
498 Remove tests from testlist that are not in the named id list.
499 If id list is empty, return empty list.
502 if testlist and args.execute:
503 target_ids = args.execute
505 if isinstance(target_ids, list) and (len(target_ids) > 0):
506 newlist = list(filter(lambda x: x['id'] in target_ids, testlist))
509 def filter_tests_by_category(args, testlist):
511 Remove tests from testlist that are not in a named category.
514 if args.category and testlist:
516 for catg in set(args.category):
519 print('considering category {}'.format(catg))
521 if catg in tc['category'] and tc['id'] not in test_ids:
523 test_ids.append(tc['id'])
527 def get_test_cases(args):
529 If a test case file is specified, retrieve tests from that file.
530 Otherwise, glob for all json files in subdirectories and load from
532 Also, if requested, filter by category, and add tests matching
538 testdirs = ['tc-tests']
541 # at least one file was specified - remove the default directory
545 if not os.path.isfile(ff):
546 print("IGNORING file " + ff + "\n\tBECAUSE does not exist.")
548 flist.append(os.path.abspath(ff))
551 testdirs = args.directory
553 for testdir in testdirs:
554 for root, dirnames, filenames in os.walk(testdir):
555 for filename in fnmatch.filter(filenames, '*.json'):
556 candidate = os.path.abspath(os.path.join(root, filename))
557 if candidate not in testdirs:
558 flist.append(candidate)
560 alltestcases = list()
561 for casefile in flist:
562 alltestcases = alltestcases + (load_from_file(casefile))
564 allcatlist = get_test_categories(alltestcases)
565 allidlist = get_id_list(alltestcases)
567 testcases_by_cats = get_categorized_testlist(alltestcases, allcatlist)
568 idtestcases = filter_tests_by_id(args, alltestcases)
569 cattestcases = filter_tests_by_category(args, alltestcases)
571 cat_ids = [x['id'] for x in cattestcases]
574 alltestcases = cattestcases + [x for x in idtestcases if x['id'] not in cat_ids]
576 alltestcases = idtestcases
579 alltestcases = cattestcases
581 # just accept the existing value of alltestcases,
582 # which has been filtered by file/directory
585 return allcatlist, allidlist, testcases_by_cats, alltestcases
588 def set_operation_mode(pm, args):
590 Load the test case data and process remaining arguments to determine
591 what the script should do for this run, and call the appropriate
594 ucat, idlist, testcases, alltests = get_test_cases(args)
597 if (has_blank_ids(idlist)):
598 alltests = generate_case_ids(alltests)
600 print("No empty ID fields found in test files.")
603 duplicate_ids = check_case_id(alltests)
604 if (len(duplicate_ids) > 0):
605 print("The following test case IDs are not unique:")
606 print(str(set(duplicate_ids)))
607 print("Please correct them before continuing.")
611 for atest in alltests:
612 print_test_case(atest)
615 if isinstance(args.category, list) and (len(args.category) == 0):
616 print("Available categories:")
622 list_test_cases(alltests)
626 catresults = test_runner(pm, args, alltests)
628 catresults = 'No tests found\n'
630 print('Tap output suppression requested\n')
632 print('All test results: \n\n{}'.format(catresults))
636 Start of execution; set up argument parser and get the arguments,
637 and start operations.
639 parser = args_parse()
640 parser = set_args(parser)
641 pm = PluginMgr(parser)
642 parser = pm.call_add_args(parser)
643 (args, remaining) = parser.parse_known_args()
645 check_default_settings(args, remaining, pm)
647 print('args is {}'.format(args))
649 set_operation_mode(pm, args)
654 if __name__ == "__main__":