Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

tools: tc-testing: Introduce plugin architecture

This should be a general test architecture, and yet allow specific
tests to be done. Introduce a plugin architecture.

An individual test has 4 stages, setup/execute/verify/teardown. Each
plugin gets a chance to run a function at each stage, plus one call
before all the tests are called ("pre" suite) and one after all the
tests are called ("post" suite). In addition, just before each
command is executed, the plugin gets a chance to modify the command
using the "adjust_command" hook. This makes the test suite quite
flexible.

Future patches will take some functionality out of the tdc.py script and
place it in plugins.

To use the plugins, place the implementation in the plugins directory
and run tdc.py. It will notice the plugins and use them.

Signed-off-by: Brenda J. Butler <bjb@mojatatu.com>
Acked-by: Lucas Bates <lucasb@mojatatu.com>
Signed-off-by: David S. Miller <davem@davemloft.net>

authored by

Brenda J. Butler and committed by
David S. Miller
93707cba 6fac733d

+368 -58
+74
tools/testing/selftests/tc-testing/TdcPlugin.py
··· 1 + #!/usr/bin/env python3 2 + 3 + class TdcPlugin: 4 + def __init__(self): 5 + super().__init__() 6 + print(' -- {}.__init__'.format(self.sub_class)) 7 + 8 + def pre_suite(self, testcount, testidlist): 9 + '''run commands before test_runner goes into a test loop''' 10 + self.testcount = testcount 11 + self.testidlist = testidlist 12 + if self.args.verbose > 1: 13 + print(' -- {}.pre_suite'.format(self.sub_class)) 14 + 15 + def post_suite(self, index): 16 + '''run commands after test_runner completes the test loop 17 + index is the last ordinal number of test that was attempted''' 18 + if self.args.verbose > 1: 19 + print(' -- {}.post_suite'.format(self.sub_class)) 20 + 21 + def pre_case(self, test_ordinal, testid): 22 + '''run commands before test_runner does one test''' 23 + if self.args.verbose > 1: 24 + print(' -- {}.pre_case'.format(self.sub_class)) 25 + self.args.testid = testid 26 + self.args.test_ordinal = test_ordinal 27 + 28 + def post_case(self): 29 + '''run commands after test_runner does one test''' 30 + if self.args.verbose > 1: 31 + print(' -- {}.post_case'.format(self.sub_class)) 32 + 33 + def pre_execute(self): 34 + '''run command before test-runner does the execute step''' 35 + if self.args.verbose > 1: 36 + print(' -- {}.pre_execute'.format(self.sub_class)) 37 + 38 + def post_execute(self): 39 + '''run command after test-runner does the execute step''' 40 + if self.args.verbose > 1: 41 + print(' -- {}.post_execute'.format(self.sub_class)) 42 + 43 + def adjust_command(self, stage, command): 44 + '''adjust the command''' 45 + if self.args.verbose > 1: 46 + print(' -- {}.adjust_command {}'.format(self.sub_class, stage)) 47 + 48 + # if stage == 'pre': 49 + # pass 50 + # elif stage == 'setup': 51 + # pass 52 + # elif stage == 'execute': 53 + # pass 54 + # elif stage == 'verify': 55 + # pass 56 + # elif stage == 'teardown': 57 + # pass 58 + # elif stage == 'post': 59 + # pass 60 + # else: 61 + # pass 62 + 63 + return command 64 + 65 + def add_args(self, parser): 66 + '''Get the plugin args from the command line''' 67 + self.argparser = parser 68 + return self.argparser 69 + 70 + def check_args(self, args, remaining): 71 + '''Check that the args are set correctly''' 72 + self.args = args 73 + if self.args.verbose > 1: 74 + print(' -- {}.check_args'.format(self.sub_class))
+104
tools/testing/selftests/tc-testing/creating-plugins/AddingPlugins.txt
··· 1 + tdc - Adding plugins for tdc 2 + 3 + Author: Brenda J. Butler - bjb@mojatatu.com 4 + 5 + ADDING PLUGINS 6 + -------------- 7 + 8 + A new plugin should be written in python as a class that inherits from TdcPlugin. 9 + There are some examples in plugin-lib. 10 + 11 + The plugin can be used to add functionality to the test framework, 12 + such as: 13 + 14 + - adding commands to be run before and/or after the test suite 15 + - adding commands to be run before and/or after the test cases 16 + - adding commands to be run before and/or after the execute phase of the test cases 17 + - ability to alter the command to be run in any phase: 18 + pre (the pre-suite stage) 19 + prepare 20 + execute 21 + verify 22 + teardown 23 + post (the post-suite stage) 24 + - ability to add to the command line args, and use them at run time 25 + 26 + 27 + The functions in the class should follow the following interfaces: 28 + 29 + def __init__(self) 30 + def pre_suite(self, testcount, testidlist) # see "PRE_SUITE" below 31 + def post_suite(self, ordinal) # see "SKIPPING" below 32 + def pre_case(self, test_ordinal, testid) # see "PRE_CASE" below 33 + def post_case(self) 34 + def pre_execute(self) 35 + def post_execute(self) 36 + def adjust_command(self, stage, command) # see "ADJUST" below 37 + def add_args(self, parser) # see "ADD_ARGS" below 38 + def check_args(self, args, remaining) # see "CHECK_ARGS" below 39 + 40 + 41 + PRE_SUITE 42 + 43 + This method takes a testcount (number of tests to be run) and 44 + testidlist (array of test ids for tests that will be run). This is 45 + useful for various things, including when an exception occurs and the 46 + rest of the tests must be skipped. The info is stored in the object, 47 + and the post_suite method can refer to it when dumping the "skipped" 48 + TAP output. The tdc.py script will do that for the test suite as 49 + defined in the test case, but if the plugin is being used to run extra 50 + tests on each test (eg, check for memory leaks on associated 51 + co-processes) then that other tap output can be generated in the 52 + post-suite method using this info passed in to the pre_suite method. 53 + 54 + 55 + SKIPPING 56 + 57 + The post_suite method will receive the ordinal number of the last 58 + test to be attempted. It can use this info when outputting 59 + the TAP output for the extra test cases. 60 + 61 + 62 + PRE_CASE 63 + 64 + The pre_case method will receive the ordinal number of the test 65 + and the test id. Useful for outputing the extra test results. 66 + 67 + 68 + ADJUST 69 + 70 + The adjust_command method receives a string representing 71 + the execution stage and a string which is the actual command to be 72 + executed. The plugin can adjust the command, based on the stage of 73 + execution. 74 + 75 + The stages are represented by the following strings: 76 + 77 + 'pre' 78 + 'setup' 79 + 'command' 80 + 'verify' 81 + 'teardown' 82 + 'post' 83 + 84 + The adjust_command method must return the adjusted command so tdc 85 + can use it. 86 + 87 + 88 + ADD_ARGS 89 + 90 + The add_args method receives the argparser object and can add 91 + arguments to it. Care should be taken that the new arguments do not 92 + conflict with any from tdc.py or from other plugins that will be used 93 + concurrently. 94 + 95 + The add_args method should return the argparser object. 96 + 97 + 98 + CHECK_ARGS 99 + 100 + The check_args method is so that the plugin can do validation on 101 + the args, if needed. If there is a problem, and Exception should 102 + be raised, with a string that explains the problem. 103 + 104 + eg: raise Exception('plugin xxx, arg -y is wrong, fix it')
+27
tools/testing/selftests/tc-testing/plugin-lib/README-PLUGINS
··· 1 + tdc.py will look for plugins in a directory plugins off the cwd. 2 + Make a set of numbered symbolic links from there to the actual plugins. 3 + Eg: 4 + 5 + tdc.py 6 + plugin-lib/ 7 + plugins/ 8 + __init__.py 9 + 10-rootPlugin.py -> ../plugin-lib/rootPlugin.py 10 + 20-valgrindPlugin.py -> ../plugin-lib/valgrindPlugin.py 11 + 30-nsPlugin.py -> ../plugin-lib/nsPlugin.py 12 + 13 + 14 + tdc.py will find them and use them. 15 + 16 + 17 + rootPlugin 18 + Check if the uid is root. If not, bail out. 19 + 20 + valgrindPlugin 21 + Run the command under test with valgrind, and produce an extra set of TAP results for the memory tests. 22 + This plugin will write files to the cwd, called vgnd-xxx.log. These will contain 23 + the valgrind output for test xxx. Any file matching the glob 'vgnd-*.log' will be 24 + deleted at the end of the run. 25 + 26 + nsPlugin 27 + Run all the commands in a network namespace.
tools/testing/selftests/tc-testing/plugins/__init__.py
+163 -58
tools/testing/selftests/tc-testing/tdc.py
··· 11 11 import os 12 12 import sys 13 13 import argparse 14 + import importlib 14 15 import json 15 16 import subprocess 17 + import time 16 18 from collections import OrderedDict 17 19 from string import Template 18 20 19 21 from tdc_config import * 20 22 from tdc_helper import * 21 23 24 + import TdcPlugin 22 25 23 26 USE_NS = True 27 + 28 + class PluginMgr: 29 + def __init__(self, argparser): 30 + super().__init__() 31 + self.plugins = {} 32 + self.plugin_instances = [] 33 + self.args = [] 34 + self.argparser = argparser 35 + 36 + # TODO, put plugins in order 37 + plugindir = os.getenv('TDC_PLUGIN_DIR', './plugins') 38 + for dirpath, dirnames, filenames in os.walk(plugindir): 39 + for fn in filenames: 40 + if (fn.endswith('.py') and 41 + not fn == '__init__.py' and 42 + not fn.startswith('#') and 43 + not fn.startswith('.#')): 44 + mn = fn[0:-3] 45 + foo = importlib.import_module('plugins.' + mn) 46 + self.plugins[mn] = foo 47 + self.plugin_instances.append(foo.SubPlugin()) 48 + 49 + def call_pre_suite(self, testcount, testidlist): 50 + for pgn_inst in self.plugin_instances: 51 + pgn_inst.pre_suite(testcount, testidlist) 52 + 53 + def call_post_suite(self, index): 54 + for pgn_inst in reversed(self.plugin_instances): 55 + pgn_inst.post_suite(index) 56 + 57 + def call_pre_case(self, test_ordinal, testid): 58 + for pgn_inst in self.plugin_instances: 59 + try: 60 + pgn_inst.pre_case(test_ordinal, testid) 61 + except Exception as ee: 62 + print('exception {} in call to pre_case for {} plugin'. 63 + format(ee, pgn_inst.__class__)) 64 + print('test_ordinal is {}'.format(test_ordinal)) 65 + print('testid is {}'.format(testid)) 66 + raise 67 + 68 + def call_post_case(self): 69 + for pgn_inst in reversed(self.plugin_instances): 70 + pgn_inst.post_case() 71 + 72 + def call_pre_execute(self): 73 + for pgn_inst in self.plugin_instances: 74 + pgn_inst.pre_execute() 75 + 76 + def call_post_execute(self): 77 + for pgn_inst in reversed(self.plugin_instances): 78 + pgn_inst.post_execute() 79 + 80 + def call_add_args(self, parser): 81 + for pgn_inst in self.plugin_instances: 82 + parser = pgn_inst.add_args(parser) 83 + return parser 84 + 85 + def call_check_args(self, args, remaining): 86 + for pgn_inst in self.plugin_instances: 87 + pgn_inst.check_args(args, remaining) 88 + 89 + def call_adjust_command(self, stage, command): 90 + for pgn_inst in self.plugin_instances: 91 + command = pgn_inst.adjust_command(stage, command) 92 + return command 93 + 94 + @staticmethod 95 + def _make_argparser(args): 96 + self.argparser = argparse.ArgumentParser( 97 + description='Linux TC unit tests') 24 98 25 99 26 100 def replace_keywords(cmd): ··· 107 33 return subcmd 108 34 109 35 110 - def exec_cmd(command, nsonly=True): 36 + def exec_cmd(args, pm, stage, command, nsonly=True): 111 37 """ 112 38 Perform any required modifications on an executable command, then run 113 39 it in a subprocess and return the results. 114 40 """ 41 + if len(command.strip()) == 0: 42 + return None, None 115 43 if (USE_NS and nsonly): 116 44 command = 'ip netns exec $NS ' + command 117 45 118 46 if '$' in command: 119 47 command = replace_keywords(command) 120 48 49 + command = pm.call_adjust_command(stage, command) 50 + if args.verbose > 0: 51 + print('command "{}"'.format(command)) 121 52 proc = subprocess.Popen(command, 122 53 shell=True, 123 54 stdout=subprocess.PIPE, 124 - stderr=subprocess.PIPE) 55 + stderr=subprocess.PIPE, 56 + env=ENVIR) 125 57 (rawout, serr) = proc.communicate() 126 58 127 59 if proc.returncode != 0 and len(serr) > 0: ··· 140 60 return proc, foutput 141 61 142 62 143 - def prepare_env(cmdlist): 63 + def prepare_env(args, pm, stage, prefix, cmdlist): 144 64 """ 145 - Execute the setup/teardown commands for a test case. Optionally 146 - terminate test execution if the command fails. 65 + Execute the setup/teardown commands for a test case. 66 + Optionally terminate test execution if the command fails. 147 67 """ 68 + if args.verbose > 0: 69 + print('{}'.format(prefix)) 148 70 for cmdinfo in cmdlist: 149 - if (type(cmdinfo) == list): 71 + if isinstance(cmdinfo, list): 150 72 exit_codes = cmdinfo[1:] 151 73 cmd = cmdinfo[0] 152 74 else: 153 75 exit_codes = [0] 154 76 cmd = cmdinfo 155 77 156 - if (len(cmd) == 0): 78 + if not cmd: 157 79 continue 158 80 159 - (proc, foutput) = exec_cmd(cmd) 81 + (proc, foutput) = exec_cmd(args, pm, stage, cmd) 160 82 161 - if proc.returncode not in exit_codes: 162 - print 163 - print("Could not execute:") 164 - print(cmd) 165 - print("\nError message:") 166 - print(foutput) 167 - print("\nAborting test run.") 168 - # ns_destroy() 169 - raise Exception('prepare_env did not complete successfully') 83 + if proc and (proc.returncode not in exit_codes): 84 + print('', file=sys.stderr) 85 + print("{} *** Could not execute: \"{}\"".format(prefix, cmd), 86 + file=sys.stderr) 87 + print("\n{} *** Error message: \"{}\"".format(prefix, foutput), 88 + file=sys.stderr) 89 + print("\n{} *** Aborting test run.".format(prefix), file=sys.stderr) 90 + print("\n\n{} *** stdout ***".format(proc.stdout), file=sys.stderr) 91 + print("\n\n{} *** stderr ***".format(proc.stderr), file=sys.stderr) 92 + raise Exception('"{}" did not complete successfully'.format(prefix)) 170 93 171 - def run_one_test(index, tidx): 94 + def run_one_test(pm, args, index, tidx): 172 95 result = True 173 96 tresult = "" 174 97 tap = "" 98 + if args.verbose > 0: 99 + print("\t====================\n=====> ", end="") 175 100 print("Test " + tidx["id"] + ": " + tidx["name"]) 176 - prepare_env(tidx["setup"]) 177 - (p, procout) = exec_cmd(tidx["cmdUnderTest"]) 101 + 102 + pm.call_pre_case(index, tidx['id']) 103 + prepare_env(args, pm, 'setup', "-----> prepare stage", tidx["setup"]) 104 + 105 + if (args.verbose > 0): 106 + print('-----> execute stage') 107 + pm.call_pre_execute() 108 + (p, procout) = exec_cmd(args, pm, 'execute', tidx["cmdUnderTest"]) 178 109 exit_code = p.returncode 110 + pm.call_post_execute() 179 111 180 112 if (exit_code != int(tidx["expExitCode"])): 181 113 result = False 182 114 print("exit:", exit_code, int(tidx["expExitCode"])) 183 115 print(procout) 184 116 else: 185 - match_pattern = re.compile(str(tidx["matchPattern"]), 186 - re.DOTALL | re.MULTILINE) 187 - (p, procout) = exec_cmd(tidx["verifyCmd"]) 117 + if args.verbose > 0: 118 + print('-----> verify stage') 119 + match_pattern = re.compile( 120 + str(tidx["matchPattern"]), re.DOTALL | re.MULTILINE) 121 + (p, procout) = exec_cmd(args, pm, 'verify', tidx["verifyCmd"]) 188 122 match_index = re.findall(match_pattern, procout) 189 123 if len(match_index) != int(tidx["matchCount"]): 190 124 result = False 191 125 192 126 if not result: 193 - tresult += "not " 194 - tresult += "ok {} - {} # {}\n".format(str(index), tidx['id'], tidx["name"]) 127 + tresult += 'not ' 128 + tresult += 'ok {} - {} # {}\n'.format(str(index), tidx['id'], tidx['name']) 195 129 tap += tresult 196 130 197 131 if result == False: 198 132 tap += procout 199 133 200 - prepare_env(tidx["teardown"]) 134 + prepare_env(args, pm, 'teardown', '-----> teardown stage', tidx['teardown']) 135 + pm.call_post_case() 136 + 201 137 index += 1 202 138 203 139 return tap 204 140 205 - def test_runner(filtered_tests, args): 141 + def test_runner(pm, args, filtered_tests): 206 142 """ 207 143 Driver function for the unit tests. 208 144 ··· 231 135 tcount = len(testlist) 232 136 index = 1 233 137 tap = str(index) + ".." + str(tcount) + "\n" 138 + badtest = None 234 139 140 + pm.call_pre_suite(tcount, [tidx['id'] for tidx in testlist]) 141 + 142 + if args.verbose > 1: 143 + print('Run tests here') 235 144 for tidx in testlist: 236 145 if "flower" in tidx["category"] and args.device == None: 237 146 continue 238 147 try: 239 148 badtest = tidx # in case it goes bad 240 - tap += run_one_test(index, tidx) 149 + tap += run_one_test(pm, args, index, tidx) 241 150 except Exception as ee: 242 151 print('Exception {} (caught in test_runner, running test {} {} {})'. 243 152 format(ee, index, tidx['id'], tidx['name'])) 244 153 break 245 154 index += 1 246 155 156 + # if we failed in setup or teardown, 157 + # fill in the remaining tests with not ok 247 158 count = index 248 159 tap += 'about to flush the tap output if tests need to be skipped\n' 249 160 if tcount + 1 != index: 250 161 for tidx in testlist[index - 1:]: 251 162 msg = 'skipped - previous setup or teardown failed' 252 - tap += 'ok {} - {} # {} {} {} \n'.format( 163 + tap += 'ok {} - {} # {} {} {}\n'.format( 253 164 count, tidx['id'], msg, index, badtest.get('id', '--Unknown--')) 254 165 count += 1 255 166 256 167 tap += 'done flushing skipped test tap output\n' 168 + pm.call_post_suite(index) 257 169 258 170 return tap 259 171 260 172 261 - def ns_create(): 173 + def ns_create(args, pm): 262 174 """ 263 175 Create the network namespace in which the tests will be run and set up 264 176 the required network devices for it. 265 177 """ 266 178 if (USE_NS): 267 179 cmd = 'ip netns add $NS' 268 - exec_cmd(cmd, False) 180 + exec_cmd(args, pm, 'pre', cmd, False) 269 181 cmd = 'ip link add $DEV0 type veth peer name $DEV1' 270 - exec_cmd(cmd, False) 182 + exec_cmd(args, pm, 'pre', cmd, False) 271 183 cmd = 'ip link set $DEV1 netns $NS' 272 - exec_cmd(cmd, False) 184 + exec_cmd(args, pm, 'pre', cmd, False) 273 185 cmd = 'ip link set $DEV0 up' 274 - exec_cmd(cmd, False) 186 + exec_cmd(args, pm, 'pre', cmd, False) 275 187 cmd = 'ip -n $NS link set $DEV1 up' 276 - exec_cmd(cmd, False) 188 + exec_cmd(args, pm, 'pre', cmd, False) 277 189 cmd = 'ip link set $DEV2 netns $NS' 278 - exec_cmd(cmd, False) 190 + exec_cmd(args, pm, 'pre', cmd, False) 279 191 cmd = 'ip -n $NS link set $DEV2 up' 280 - exec_cmd(cmd, False) 192 + exec_cmd(args, pm, 'pre', cmd, False) 281 193 282 194 283 - def ns_destroy(): 195 + def ns_destroy(args, pm): 284 196 """ 285 197 Destroy the network namespace for testing (and any associated network 286 198 devices as well) 287 199 """ 288 200 if (USE_NS): 289 201 cmd = 'ip netns delete $NS' 290 - exec_cmd(cmd, False) 202 + exec_cmd(args, pm, 'post', cmd, False) 291 203 292 204 293 205 def has_blank_ids(idlist): ··· 376 272 return parser 377 273 378 274 379 - def check_default_settings(args): 275 + def check_default_settings(args, remaining, pm): 380 276 """ 381 - Process any arguments overriding the default settings, and ensure the 382 - settings are correct. 277 + Process any arguments overriding the default settings, 278 + and ensure the settings are correct. 383 279 """ 384 280 # Allow for overriding specific settings 385 281 global NAMES ··· 391 287 if not os.path.isfile(NAMES['TC']): 392 288 print("The specified tc path " + NAMES['TC'] + " does not exist.") 393 289 exit(1) 290 + 291 + pm.call_check_args(args, remaining) 394 292 395 293 396 294 def get_id_list(alltests): ··· 407 301 Check for duplicate test case IDs. 408 302 """ 409 303 idl = get_id_list(alltests) 410 - # print('check_case_id: idl is {}'.format(idl)) 411 - # answer = list() 412 - # for x in idl: 413 - # print('Looking at {}'.format(x)) 414 - # print('what the heck is idl.count(x)??? {}'.format(idl.count(x))) 415 - # if idl.count(x) > 1: 416 - # answer.append(x) 417 - # print(' ... append it {}'.format(x)) 418 304 return [x for x in idl if idl.count(x) > 1] 419 - return answer 420 305 421 306 422 307 def does_id_exist(alltests, newid): ··· 500 403 501 404 for ff in args.file: 502 405 if not os.path.isfile(ff): 503 - print("IGNORING file " + ff + " \n\tBECAUSE does not exist.") 406 + print("IGNORING file " + ff + "\n\tBECAUSE does not exist.") 504 407 else: 505 408 flist.append(os.path.abspath(ff)) 506 409 ··· 542 445 return allcatlist, allidlist, testcases_by_cats, alltestcases 543 446 544 447 545 - def set_operation_mode(args): 448 + def set_operation_mode(pm, args): 546 449 """ 547 450 Load the test case data and process remaining arguments to determine 548 451 what the script should do for this run, and call the appropriate ··· 583 486 print("This script must be run with root privileges.\n") 584 487 exit(1) 585 488 586 - ns_create() 489 + ns_create(args, pm) 587 490 588 - catresults = test_runner(alltests, args) 491 + if len(alltests): 492 + catresults = test_runner(pm, args, alltests) 493 + else: 494 + catresults = 'No tests found\n' 589 495 print('All test results: \n\n{}'.format(catresults)) 590 496 591 - ns_destroy() 497 + ns_destroy(args, pm) 592 498 593 499 594 500 def main(): ··· 601 501 """ 602 502 parser = args_parse() 603 503 parser = set_args(parser) 504 + pm = PluginMgr(parser) 505 + parser = pm.call_add_args(parser) 604 506 (args, remaining) = parser.parse_known_args() 605 - check_default_settings(args) 507 + args.NAMES = NAMES 508 + check_default_settings(args, remaining, pm) 509 + if args.verbose > 2: 510 + print('args is {}'.format(args)) 606 511 607 - set_operation_mode(args) 512 + set_operation_mode(pm, args) 608 513 609 514 exit(0) 610 515