Linux Audio

Check our new training course

Loading...
   1#!/usr/bin/env python3
   2# SPDX-License-Identifier: GPL-2.0
   3
   4"""
   5tdc.py - Linux tc (Traffic Control) unit test driver
   6
   7Copyright (C) 2017 Lucas Bates <lucasb@mojatatu.com>
   8"""
   9
  10import re
  11import os
  12import sys
  13import argparse
  14import importlib
  15import json
  16import subprocess
  17import time
  18import traceback
  19import random
  20from multiprocessing import Pool
  21from collections import OrderedDict
  22from string import Template
  23
  24from tdc_config import *
  25from tdc_helper import *
  26
  27import TdcPlugin
  28from TdcResults import *
  29
  30class PluginDependencyException(Exception):
  31    def __init__(self, missing_pg):
  32        self.missing_pg = missing_pg
  33
  34class PluginMgrTestFail(Exception):
  35    def __init__(self, stage, output, message):
  36        self.stage = stage
  37        self.output = output
  38        self.message = message
  39
  40class PluginMgr:
  41    def __init__(self, argparser):
  42        super().__init__()
  43        self.plugins = set()
  44        self.plugin_instances = []
  45        self.failed_plugins = {}
  46        self.argparser = argparser
  47
  48        plugindir = os.getenv('TDC_PLUGIN_DIR', './plugins')
  49        for dirpath, dirnames, filenames in os.walk(plugindir):
  50            for fn in filenames:
  51                if (fn.endswith('.py') and
  52                    not fn == '__init__.py' and
  53                    not fn.startswith('#') and
  54                    not fn.startswith('.#')):
  55                    mn = fn[0:-3]
  56                    foo = importlib.import_module('plugins.' + mn)
  57                    self.plugins.add(mn)
  58                    self.plugin_instances[mn] = foo.SubPlugin()
  59
  60    def load_plugin(self, pgdir, pgname):
  61        pgname = pgname[0:-3]
  62        self.plugins.add(pgname)
  63
  64        foo = importlib.import_module('{}.{}'.format(pgdir, pgname))
  65
  66        # nsPlugin must always be the first one
  67        if pgname == "nsPlugin":
  68            self.plugin_instances.insert(0, (pgname, foo.SubPlugin()))
  69            self.plugin_instances[0][1].check_args(self.args, None)
  70        else:
  71            self.plugin_instances.append((pgname, foo.SubPlugin()))
  72            self.plugin_instances[-1][1].check_args(self.args, None)
  73
  74    def get_required_plugins(self, testlist):
  75        '''
  76        Get all required plugins from the list of test cases and return
  77        all unique items.
  78        '''
  79        reqs = set()
  80        for t in testlist:
  81            try:
  82                if 'requires' in t['plugins']:
  83                    if isinstance(t['plugins']['requires'], list):
  84                        reqs.update(set(t['plugins']['requires']))
  85                    else:
  86                        reqs.add(t['plugins']['requires'])
  87                    t['plugins'] = t['plugins']['requires']
  88                else:
  89                    t['plugins'] = []
  90            except KeyError:
  91                t['plugins'] = []
  92                continue
  93
  94        return reqs
  95
  96    def load_required_plugins(self, reqs, parser, args, remaining):
  97        '''
  98        Get all required plugins from the list of test cases and load any plugin
  99        that is not already enabled.
 100        '''
 101        pgd = ['plugin-lib', 'plugin-lib-custom']
 102        pnf = []
 103
 104        for r in reqs:
 105            if r not in self.plugins:
 106                fname = '{}.py'.format(r)
 107                source_path = []
 108                for d in pgd:
 109                    pgpath = '{}/{}'.format(d, fname)
 110                    if os.path.isfile(pgpath):
 111                        source_path.append(pgpath)
 112                if len(source_path) == 0:
 113                    print('ERROR: unable to find required plugin {}'.format(r))
 114                    pnf.append(fname)
 115                    continue
 116                elif len(source_path) > 1:
 117                    print('WARNING: multiple copies of plugin {} found, using version found')
 118                    print('at {}'.format(source_path[0]))
 119                pgdir = source_path[0]
 120                pgdir = pgdir.split('/')[0]
 121                self.load_plugin(pgdir, fname)
 122        if len(pnf) > 0:
 123            raise PluginDependencyException(pnf)
 124
 125        parser = self.call_add_args(parser)
 126        (args, remaining) = parser.parse_known_args(args=remaining, namespace=args)
 127        return args
 128
 129    def call_pre_suite(self, testcount, testidlist):
 130        for (_, pgn_inst) in self.plugin_instances:
 131            pgn_inst.pre_suite(testcount, testidlist)
 132
 133    def call_post_suite(self, index):
 134        for (_, pgn_inst) in reversed(self.plugin_instances):
 135            pgn_inst.post_suite(index)
 136
 137    def call_pre_case(self, caseinfo, *, test_skip=False):
 138        for (pgn, pgn_inst) in self.plugin_instances:
 139            if pgn not in caseinfo['plugins']:
 140                continue
 141            try:
 142                pgn_inst.pre_case(caseinfo, test_skip)
 143            except Exception as ee:
 144                print('exception {} in call to pre_case for {} plugin'.
 145                      format(ee, pgn_inst.__class__))
 146                print('test_ordinal is {}'.format(test_ordinal))
 147                print('testid is {}'.format(caseinfo['id']))
 148                raise
 149
 150    def call_post_case(self, caseinfo):
 151        for (pgn, pgn_inst) in reversed(self.plugin_instances):
 152            if pgn not in caseinfo['plugins']:
 153                continue
 154            pgn_inst.post_case()
 155
 156    def call_pre_execute(self, caseinfo):
 157        for (pgn, pgn_inst) in self.plugin_instances:
 158            if pgn not in caseinfo['plugins']:
 159                continue
 160            pgn_inst.pre_execute()
 161
 162    def call_post_execute(self, caseinfo):
 163        for (pgn, pgn_inst) in reversed(self.plugin_instances):
 164            if pgn not in caseinfo['plugins']:
 165                continue
 166            pgn_inst.post_execute()
 167
 168    def call_add_args(self, parser):
 169        for (pgn, pgn_inst) in self.plugin_instances:
 170            parser = pgn_inst.add_args(parser)
 171        return parser
 172
 173    def call_check_args(self, args, remaining):
 174        for (pgn, pgn_inst) in self.plugin_instances:
 175            pgn_inst.check_args(args, remaining)
 176
 177    def call_adjust_command(self, caseinfo, stage, command):
 178        for (pgn, pgn_inst) in self.plugin_instances:
 179            if pgn not in caseinfo['plugins']:
 180                continue
 181            command = pgn_inst.adjust_command(stage, command)
 182        return command
 183
 184    def set_args(self, args):
 185        self.args = args
 186
 187    @staticmethod
 188    def _make_argparser(args):
 189        self.argparser = argparse.ArgumentParser(
 190            description='Linux TC unit tests')
 191
 192def replace_keywords(cmd):
 193    """
 194    For a given executable command, substitute any known
 195    variables contained within NAMES with the correct values
 196    """
 197    tcmd = Template(cmd)
 198    subcmd = tcmd.safe_substitute(NAMES)
 199    return subcmd
 200
 201
 202def exec_cmd(caseinfo, args, pm, stage, command):
 203    """
 204    Perform any required modifications on an executable command, then run
 205    it in a subprocess and return the results.
 206    """
 207    if len(command.strip()) == 0:
 208        return None, None
 209    if '$' in command:
 210        command = replace_keywords(command)
 211
 212    command = pm.call_adjust_command(caseinfo, stage, command)
 213    if args.verbose > 0:
 214        print('command "{}"'.format(command))
 215
 216    proc = subprocess.Popen(command,
 217        shell=True,
 218        stdout=subprocess.PIPE,
 219        stderr=subprocess.PIPE,
 220        env=ENVIR)
 221
 222    try:
 223        (rawout, serr) = proc.communicate(timeout=NAMES['TIMEOUT'])
 224        if proc.returncode != 0 and len(serr) > 0:
 225            foutput = serr.decode("utf-8", errors="ignore")
 226        else:
 227            foutput = rawout.decode("utf-8", errors="ignore")
 228    except subprocess.TimeoutExpired:
 229        foutput = "Command \"{}\" timed out\n".format(command)
 230        proc.returncode = 255
 231
 232    proc.stdout.close()
 233    proc.stderr.close()
 234    return proc, foutput
 235
 236
 237def prepare_env(caseinfo, args, pm, stage, prefix, cmdlist, output = None):
 238    """
 239    Execute the setup/teardown commands for a test case.
 240    Optionally terminate test execution if the command fails.
 241    """
 242    if args.verbose > 0:
 243        print('{}'.format(prefix))
 244    for cmdinfo in cmdlist:
 245        if isinstance(cmdinfo, list):
 246            exit_codes = cmdinfo[1:]
 247            cmd = cmdinfo[0]
 248        else:
 249            exit_codes = [0]
 250            cmd = cmdinfo
 251
 252        if not cmd:
 253            continue
 254
 255        (proc, foutput) = exec_cmd(caseinfo, args, pm, stage, cmd)
 256
 257        if proc and (proc.returncode not in exit_codes):
 258            print('', file=sys.stderr)
 259            print("{} *** Could not execute: \"{}\"".format(prefix, cmd),
 260                  file=sys.stderr)
 261            print("\n{} *** Error message: \"{}\"".format(prefix, foutput),
 262                  file=sys.stderr)
 263            print("returncode {}; expected {}".format(proc.returncode,
 264                                                      exit_codes))
 265            print("\n{} *** Aborting test run.".format(prefix), file=sys.stderr)
 266            print("\n\n{} *** stdout ***".format(proc.stdout), file=sys.stderr)
 267            print("\n\n{} *** stderr ***".format(proc.stderr), file=sys.stderr)
 268            raise PluginMgrTestFail(
 269                stage, output,
 270                '"{}" did not complete successfully'.format(prefix))
 271
 272def verify_by_json(procout, res, tidx, args, pm):
 273    try:
 274        outputJSON = json.loads(procout)
 275    except json.JSONDecodeError:
 276        res.set_result(ResultState.fail)
 277        res.set_failmsg('Cannot decode verify command\'s output. Is it JSON?')
 278        return res
 279
 280    matchJSON = json.loads(json.dumps(tidx['matchJSON']))
 281
 282    if type(outputJSON) != type(matchJSON):
 283        failmsg = 'Original output and matchJSON value are not the same type: output: {} != matchJSON: {} '
 284        failmsg = failmsg.format(type(outputJSON).__name__, type(matchJSON).__name__)
 285        res.set_result(ResultState.fail)
 286        res.set_failmsg(failmsg)
 287        return res
 288
 289    if len(matchJSON) > len(outputJSON):
 290        failmsg = "Your matchJSON value is an array, and it contains more elements than the command under test\'s output:\ncommand output (length: {}):\n{}\nmatchJSON value (length: {}):\n{}"
 291        failmsg = failmsg.format(len(outputJSON), outputJSON, len(matchJSON), matchJSON)
 292        res.set_result(ResultState.fail)
 293        res.set_failmsg(failmsg)
 294        return res
 295    res = find_in_json(res, outputJSON, matchJSON, 0)
 296
 297    return res
 298
 299def find_in_json(res, outputJSONVal, matchJSONVal, matchJSONKey=None):
 300    if res.get_result() == ResultState.fail:
 301        return res
 302
 303    if type(matchJSONVal) == list:
 304        res = find_in_json_list(res, outputJSONVal, matchJSONVal, matchJSONKey)
 305
 306    elif type(matchJSONVal) == dict:
 307        res = find_in_json_dict(res, outputJSONVal, matchJSONVal)
 308    else:
 309        res = find_in_json_other(res, outputJSONVal, matchJSONVal, matchJSONKey)
 310
 311    if res.get_result() != ResultState.fail:
 312        res.set_result(ResultState.success)
 313        return res
 314
 315    return res
 316
 317def find_in_json_list(res, outputJSONVal, matchJSONVal, matchJSONKey=None):
 318    if (type(matchJSONVal) != type(outputJSONVal)):
 319        failmsg = 'Original output and matchJSON value are not the same type: output: {} != matchJSON: {}'
 320        failmsg = failmsg.format(outputJSONVal, matchJSONVal)
 321        res.set_result(ResultState.fail)
 322        res.set_failmsg(failmsg)
 323        return res
 324
 325    if len(matchJSONVal) > len(outputJSONVal):
 326        failmsg = "Your matchJSON value is an array, and it contains more elements than the command under test\'s output:\ncommand output (length: {}):\n{}\nmatchJSON value (length: {}):\n{}"
 327        failmsg = failmsg.format(len(outputJSONVal), outputJSONVal, len(matchJSONVal), matchJSONVal)
 328        res.set_result(ResultState.fail)
 329        res.set_failmsg(failmsg)
 330        return res
 331
 332    for matchJSONIdx, matchJSONVal in enumerate(matchJSONVal):
 333        res = find_in_json(res, outputJSONVal[matchJSONIdx], matchJSONVal,
 334                           matchJSONKey)
 335    return res
 336
 337def find_in_json_dict(res, outputJSONVal, matchJSONVal):
 338    for matchJSONKey, matchJSONVal in matchJSONVal.items():
 339        if type(outputJSONVal) == dict:
 340            if matchJSONKey not in outputJSONVal:
 341                failmsg = 'Key not found in json output: {}: {}\nMatching against output: {}'
 342                failmsg = failmsg.format(matchJSONKey, matchJSONVal, outputJSONVal)
 343                res.set_result(ResultState.fail)
 344                res.set_failmsg(failmsg)
 345                return res
 346
 347        else:
 348            failmsg = 'Original output and matchJSON value are not the same type: output: {} != matchJSON: {}'
 349            failmsg = failmsg.format(type(outputJSON).__name__, type(matchJSON).__name__)
 350            res.set_result(ResultState.fail)
 351            res.set_failmsg(failmsg)
 352            return rest
 353
 354        if type(outputJSONVal) == dict and (type(outputJSONVal[matchJSONKey]) == dict or
 355                type(outputJSONVal[matchJSONKey]) == list):
 356            if len(matchJSONVal) > 0:
 357                res = find_in_json(res, outputJSONVal[matchJSONKey], matchJSONVal, matchJSONKey)
 358            # handling corner case where matchJSONVal == [] or matchJSONVal == {}
 359            else:
 360                res = find_in_json_other(res, outputJSONVal, matchJSONVal, matchJSONKey)
 361        else:
 362            res = find_in_json(res, outputJSONVal, matchJSONVal, matchJSONKey)
 363    return res
 364
 365def find_in_json_other(res, outputJSONVal, matchJSONVal, matchJSONKey=None):
 366    if matchJSONKey in outputJSONVal:
 367        if matchJSONVal != outputJSONVal[matchJSONKey]:
 368            failmsg = 'Value doesn\'t match: {}: {} != {}\nMatching against output: {}'
 369            failmsg = failmsg.format(matchJSONKey, matchJSONVal, outputJSONVal[matchJSONKey], outputJSONVal)
 370            res.set_result(ResultState.fail)
 371            res.set_failmsg(failmsg)
 372            return res
 373
 374    return res
 375
 376def run_one_test(pm, args, index, tidx):
 377    global NAMES
 378    ns = NAMES['NS']
 379    dev0 = NAMES['DEV0']
 380    dev1 = NAMES['DEV1']
 381    dummy = NAMES['DUMMY']
 382    result = True
 383    tresult = ""
 384    tap = ""
 385    res = TestResult(tidx['id'], tidx['name'])
 386    if args.verbose > 0:
 387        print("\t====================\n=====> ", end="")
 388    print("Test " + tidx["id"] + ": " + tidx["name"])
 389
 390    if 'skip' in tidx:
 391        if tidx['skip'] == 'yes':
 392            res = TestResult(tidx['id'], tidx['name'])
 393            res.set_result(ResultState.skip)
 394            res.set_errormsg('Test case designated as skipped.')
 395            pm.call_pre_case(tidx, test_skip=True)
 396            pm.call_post_execute(tidx)
 397            return res
 398
 399    if 'dependsOn' in tidx:
 400        if (args.verbose > 0):
 401            print('probe command for test skip')
 402        (p, procout) = exec_cmd(tidx, args, pm, 'execute', tidx['dependsOn'])
 403        if p:
 404            if (p.returncode != 0):
 405                res = TestResult(tidx['id'], tidx['name'])
 406                res.set_result(ResultState.skip)
 407                res.set_errormsg('probe command: test skipped.')
 408                pm.call_pre_case(tidx, test_skip=True)
 409                pm.call_post_execute(tidx)
 410                return res
 411
 412    # populate NAMES with TESTID for this test
 413    NAMES['TESTID'] = tidx['id']
 414    NAMES['NS'] = '{}-{}'.format(NAMES['NS'], tidx['random'])
 415    NAMES['DEV0'] = '{}id{}'.format(NAMES['DEV0'], tidx['id'])
 416    NAMES['DEV1'] = '{}id{}'.format(NAMES['DEV1'], tidx['id'])
 417    NAMES['DUMMY'] = '{}id{}'.format(NAMES['DUMMY'], tidx['id'])
 418
 419    pm.call_pre_case(tidx)
 420    prepare_env(tidx, args, pm, 'setup', "-----> prepare stage", tidx["setup"])
 421
 422    if (args.verbose > 0):
 423        print('-----> execute stage')
 424    pm.call_pre_execute(tidx)
 425    (p, procout) = exec_cmd(tidx, args, pm, 'execute', tidx["cmdUnderTest"])
 426    if p:
 427        exit_code = p.returncode
 428    else:
 429        exit_code = None
 430
 431    pm.call_post_execute(tidx)
 432
 433    if (exit_code is None or exit_code != int(tidx["expExitCode"])):
 434        print("exit: {!r}".format(exit_code))
 435        print("exit: {}".format(int(tidx["expExitCode"])))
 436        #print("exit: {!r} {}".format(exit_code, int(tidx["expExitCode"])))
 437        res.set_result(ResultState.fail)
 438        res.set_failmsg('Command exited with {}, expected {}\n{}'.format(exit_code, tidx["expExitCode"], procout))
 439        print(procout)
 440    else:
 441        if args.verbose > 0:
 442            print('-----> verify stage')
 443        (p, procout) = exec_cmd(tidx, args, pm, 'verify', tidx["verifyCmd"])
 444        if procout:
 445            if 'matchJSON' in tidx:
 446                verify_by_json(procout, res, tidx, args, pm)
 447            elif 'matchPattern' in tidx:
 448                match_pattern = re.compile(
 449                    str(tidx["matchPattern"]), re.DOTALL | re.MULTILINE)
 450                match_index = re.findall(match_pattern, procout)
 451                if len(match_index) != int(tidx["matchCount"]):
 452                    res.set_result(ResultState.fail)
 453                    res.set_failmsg('Could not match regex pattern. Verify command output:\n{}'.format(procout))
 454                else:
 455                    res.set_result(ResultState.success)
 456            else:
 457                res.set_result(ResultState.fail)
 458                res.set_failmsg('Must specify a match option: matchJSON or matchPattern\n{}'.format(procout))
 459        elif int(tidx["matchCount"]) != 0:
 460            res.set_result(ResultState.fail)
 461            res.set_failmsg('No output generated by verify command.')
 462        else:
 463            res.set_result(ResultState.success)
 464
 465    prepare_env(tidx, args, pm, 'teardown', '-----> teardown stage', tidx['teardown'], procout)
 466    pm.call_post_case(tidx)
 467
 468    index += 1
 469
 470    # remove TESTID from NAMES
 471    del(NAMES['TESTID'])
 472
 473    # Restore names
 474    NAMES['NS'] = ns
 475    NAMES['DEV0'] = dev0
 476    NAMES['DEV1'] = dev1
 477    NAMES['DUMMY'] = dummy
 478
 479    return res
 480
 481def prepare_run(pm, args, testlist):
 482    tcount = len(testlist)
 483    emergency_exit = False
 484    emergency_exit_message = ''
 485
 486    try:
 487        pm.call_pre_suite(tcount, testlist)
 488    except Exception as ee:
 489        ex_type, ex, ex_tb = sys.exc_info()
 490        print('Exception {} {} (caught in pre_suite).'.
 491              format(ex_type, ex))
 492        traceback.print_tb(ex_tb)
 493        emergency_exit_message = 'EMERGENCY EXIT, call_pre_suite failed with exception {} {}\n'.format(ex_type, ex)
 494        emergency_exit = True
 495
 496    if emergency_exit:
 497        pm.call_post_suite(1)
 498        return emergency_exit_message
 499
 500def purge_run(pm, index):
 501    pm.call_post_suite(index)
 502
 503def test_runner(pm, args, filtered_tests):
 504    """
 505    Driver function for the unit tests.
 506
 507    Prints information about the tests being run, executes the setup and
 508    teardown commands and the command under test itself. Also determines
 509    success/failure based on the information in the test case and generates
 510    TAP output accordingly.
 511    """
 512    testlist = filtered_tests
 513    tcount = len(testlist)
 514    index = 1
 515    tap = ''
 516    badtest = None
 517    stage = None
 518
 519    tsr = TestSuiteReport()
 520
 521    for tidx in testlist:
 522        if "flower" in tidx["category"] and args.device == None:
 523            errmsg = "Tests using the DEV2 variable must define the name of a "
 524            errmsg += "physical NIC with the -d option when running tdc.\n"
 525            errmsg += "Test has been skipped."
 526            if args.verbose > 1:
 527                print(errmsg)
 528            res = TestResult(tidx['id'], tidx['name'])
 529            res.set_result(ResultState.skip)
 530            res.set_errormsg(errmsg)
 531            tsr.add_resultdata(res)
 532            index += 1
 533            continue
 534        try:
 535            badtest = tidx  # in case it goes bad
 536            res = run_one_test(pm, args, index, tidx)
 537            tsr.add_resultdata(res)
 538        except PluginMgrTestFail as pmtf:
 539            ex_type, ex, ex_tb = sys.exc_info()
 540            stage = pmtf.stage
 541            message = pmtf.message
 542            output = pmtf.output
 543            res = TestResult(tidx['id'], tidx['name'])
 544            res.set_result(ResultState.fail)
 545            res.set_errormsg(pmtf.message)
 546            res.set_failmsg(pmtf.output)
 547            tsr.add_resultdata(res)
 548            index += 1
 549            print(message)
 550            print('Exception {} {} (caught in test_runner, running test {} {} {} stage {})'.
 551                  format(ex_type, ex, index, tidx['id'], tidx['name'], stage))
 552            print('---------------')
 553            print('traceback')
 554            traceback.print_tb(ex_tb)
 555            print('---------------')
 556            if stage == 'teardown':
 557                print('accumulated output for this test:')
 558                if pmtf.output:
 559                    print(pmtf.output)
 560            print('---------------')
 561            break
 562        index += 1
 563
 564    # if we failed in setup or teardown,
 565    # fill in the remaining tests with ok-skipped
 566    count = index
 567
 568    if tcount + 1 != count:
 569        for tidx in testlist[count - 1:]:
 570            res = TestResult(tidx['id'], tidx['name'])
 571            res.set_result(ResultState.skip)
 572            msg = 'skipped - previous {} failed {} {}'.format(stage,
 573                index, badtest.get('id', '--Unknown--'))
 574            res.set_errormsg(msg)
 575            tsr.add_resultdata(res)
 576            count += 1
 577
 578    if args.pause:
 579        print('Want to pause\nPress enter to continue ...')
 580        if input(sys.stdin):
 581            print('got something on stdin')
 582
 583    return (index, tsr)
 584
 585def mp_bins(alltests):
 586    serial = []
 587    parallel = []
 588
 589    for test in alltests:
 590        if 'nsPlugin' not in test['plugins']:
 591            serial.append(test)
 592        else:
 593            # We can only create one netdevsim device at a time
 594            if 'netdevsim/new_device' in str(test['setup']):
 595                serial.append(test)
 596            else:
 597                parallel.append(test)
 598
 599    return (serial, parallel)
 600
 601def __mp_runner(tests):
 602    (_, tsr) = test_runner(mp_pm, mp_args, tests)
 603    return tsr._testsuite
 604
 605def test_runner_mp(pm, args, alltests):
 606    prepare_run(pm, args, alltests)
 607
 608    (serial, parallel) = mp_bins(alltests)
 609
 610    batches = [parallel[n : n + 32] for n in range(0, len(parallel), 32)]
 611    batches.insert(0, serial)
 612
 613    print("Executing {} tests in parallel and {} in serial".format(len(parallel), len(serial)))
 614    print("Using {} batches and {} workers".format(len(batches), args.mp))
 615
 616    # We can't pickle these objects so workaround them
 617    global mp_pm
 618    mp_pm = pm
 619
 620    global mp_args
 621    mp_args = args
 622
 623    with Pool(args.mp) as p:
 624        pres = p.map(__mp_runner, batches)
 625
 626    tsr = TestSuiteReport()
 627    for trs in pres:
 628        for res in trs:
 629            tsr.add_resultdata(res)
 630
 631    # Passing an index is not useful in MP
 632    purge_run(pm, None)
 633
 634    return tsr
 635
 636def test_runner_serial(pm, args, alltests):
 637    prepare_run(pm, args, alltests)
 638
 639    if args.verbose:
 640        print("Executing {} tests in serial".format(len(alltests)))
 641
 642    (index, tsr) = test_runner(pm, args, alltests)
 643
 644    purge_run(pm, index)
 645
 646    return tsr
 647
 648def has_blank_ids(idlist):
 649    """
 650    Search the list for empty ID fields and return true/false accordingly.
 651    """
 652    return not(all(k for k in idlist))
 653
 654
 655def load_from_file(filename):
 656    """
 657    Open the JSON file containing the test cases and return them
 658    as list of ordered dictionary objects.
 659    """
 660    try:
 661        with open(filename) as test_data:
 662            testlist = json.load(test_data, object_pairs_hook=OrderedDict)
 663    except json.JSONDecodeError as jde:
 664        print('IGNORING test case file {}\n\tBECAUSE:  {}'.format(filename, jde))
 665        testlist = list()
 666    else:
 667        idlist = get_id_list(testlist)
 668        if (has_blank_ids(idlist)):
 669            for k in testlist:
 670                k['filename'] = filename
 671    return testlist
 672
 673def identity(string):
 674    return string
 675
 676def args_parse():
 677    """
 678    Create the argument parser.
 679    """
 680    parser = argparse.ArgumentParser(description='Linux TC unit tests')
 681    parser.register('type', None, identity)
 682    return parser
 683
 684
 685def set_args(parser):
 686    """
 687    Set the command line arguments for tdc.
 688    """
 689    parser.add_argument(
 690        '--outfile', type=str,
 691        help='Path to the file in which results should be saved. ' +
 692        'Default target is the current directory.')
 693    parser.add_argument(
 694        '-p', '--path', type=str,
 695        help='The full path to the tc executable to use')
 696    sg = parser.add_argument_group(
 697        'selection', 'select which test cases: ' +
 698        'files plus directories; filtered by categories plus testids')
 699    ag = parser.add_argument_group(
 700        'action', 'select action to perform on selected test cases')
 701
 702    sg.add_argument(
 703        '-D', '--directory', nargs='+', metavar='DIR',
 704        help='Collect tests from the specified directory(ies) ' +
 705        '(default [tc-tests])')
 706    sg.add_argument(
 707        '-f', '--file', nargs='+', metavar='FILE',
 708        help='Run tests from the specified file(s)')
 709    sg.add_argument(
 710        '-c', '--category', nargs='*', metavar='CATG', default=['+c'],
 711        help='Run tests only from the specified category/ies, ' +
 712        'or if no category/ies is/are specified, list known categories.')
 713    sg.add_argument(
 714        '-e', '--execute', nargs='+', metavar='ID',
 715        help='Execute the specified test cases with specified IDs')
 716    ag.add_argument(
 717        '-l', '--list', action='store_true',
 718        help='List all test cases, or those only within the specified category')
 719    ag.add_argument(
 720        '-s', '--show', action='store_true', dest='showID',
 721        help='Display the selected test cases')
 722    ag.add_argument(
 723        '-i', '--id', action='store_true', dest='gen_id',
 724        help='Generate ID numbers for new test cases')
 725    parser.add_argument(
 726        '-v', '--verbose', action='count', default=0,
 727        help='Show the commands that are being run')
 728    parser.add_argument(
 729        '--format', default='tap', const='tap', nargs='?',
 730        choices=['none', 'xunit', 'tap'],
 731        help='Specify the format for test results. (Default: TAP)')
 732    parser.add_argument('-d', '--device',
 733                        help='Execute test cases that use a physical device, ' +
 734                        'where DEVICE is its name. (If not defined, tests ' +
 735                        'that require a physical device will be skipped)')
 736    parser.add_argument(
 737        '-P', '--pause', action='store_true',
 738        help='Pause execution just before post-suite stage')
 739    parser.add_argument(
 740        '-J', '--multiprocess', type=int, default=1, dest='mp',
 741        help='Run tests in parallel whenever possible')
 742    return parser
 743
 744
 745def check_default_settings(args, remaining, pm):
 746    """
 747    Process any arguments overriding the default settings,
 748    and ensure the settings are correct.
 749    """
 750    # Allow for overriding specific settings
 751    global NAMES
 752
 753    if args.path != None:
 754        NAMES['TC'] = args.path
 755    if args.device != None:
 756        NAMES['DEV2'] = args.device
 757    if 'TIMEOUT' not in NAMES:
 758        NAMES['TIMEOUT'] = None
 759    if not os.path.isfile(NAMES['TC']):
 760        print("The specified tc path " + NAMES['TC'] + " does not exist.")
 761        exit(1)
 762
 763    pm.call_check_args(args, remaining)
 764
 765
 766def get_id_list(alltests):
 767    """
 768    Generate a list of all IDs in the test cases.
 769    """
 770    return [x["id"] for x in alltests]
 771
 772def check_case_id(alltests):
 773    """
 774    Check for duplicate test case IDs.
 775    """
 776    idl = get_id_list(alltests)
 777    return [x for x in idl if idl.count(x) > 1]
 778
 779
 780def does_id_exist(alltests, newid):
 781    """
 782    Check if a given ID already exists in the list of test cases.
 783    """
 784    idl = get_id_list(alltests)
 785    return (any(newid == x for x in idl))
 786
 787
 788def generate_case_ids(alltests):
 789    """
 790    If a test case has a blank ID field, generate a random hex ID for it
 791    and then write the test cases back to disk.
 792    """
 793    for c in alltests:
 794        if (c["id"] == ""):
 795            while True:
 796                newid = str('{:04x}'.format(random.randrange(16**4)))
 797                if (does_id_exist(alltests, newid)):
 798                    continue
 799                else:
 800                    c['id'] = newid
 801                    break
 802
 803    ufilename = []
 804    for c in alltests:
 805        if ('filename' in c):
 806            ufilename.append(c['filename'])
 807    ufilename = get_unique_item(ufilename)
 808    for f in ufilename:
 809        testlist = []
 810        for t in alltests:
 811            if 'filename' in t:
 812                if t['filename'] == f:
 813                    del t['filename']
 814                    testlist.append(t)
 815        outfile = open(f, "w")
 816        json.dump(testlist, outfile, indent=4)
 817        outfile.write("\n")
 818        outfile.close()
 819
 820def filter_tests_by_id(args, testlist):
 821    '''
 822    Remove tests from testlist that are not in the named id list.
 823    If id list is empty, return empty list.
 824    '''
 825    newlist = list()
 826    if testlist and args.execute:
 827        target_ids = args.execute
 828
 829        if isinstance(target_ids, list) and (len(target_ids) > 0):
 830            newlist = list(filter(lambda x: x['id'] in target_ids, testlist))
 831    return newlist
 832
 833def filter_tests_by_category(args, testlist):
 834    '''
 835    Remove tests from testlist that are not in a named category.
 836    '''
 837    answer = list()
 838    if args.category and testlist:
 839        test_ids = list()
 840        for catg in set(args.category):
 841            if catg == '+c':
 842                continue
 843            print('considering category {}'.format(catg))
 844            for tc in testlist:
 845                if catg in tc['category'] and tc['id'] not in test_ids:
 846                    answer.append(tc)
 847                    test_ids.append(tc['id'])
 848
 849    return answer
 850
 851def set_random(alltests):
 852    for tidx in alltests:
 853        tidx['random'] = random.getrandbits(32)
 854
 855def get_test_cases(args):
 856    """
 857    If a test case file is specified, retrieve tests from that file.
 858    Otherwise, glob for all json files in subdirectories and load from
 859    each one.
 860    Also, if requested, filter by category, and add tests matching
 861    certain ids.
 862    """
 863    import fnmatch
 864
 865    flist = []
 866    testdirs = ['tc-tests']
 867
 868    if args.file:
 869        # at least one file was specified - remove the default directory
 870        testdirs = []
 871
 872        for ff in args.file:
 873            if not os.path.isfile(ff):
 874                print("IGNORING file " + ff + "\n\tBECAUSE does not exist.")
 875            else:
 876                flist.append(os.path.abspath(ff))
 877
 878    if args.directory:
 879        testdirs = args.directory
 880
 881    for testdir in testdirs:
 882        for root, dirnames, filenames in os.walk(testdir):
 883            for filename in fnmatch.filter(filenames, '*.json'):
 884                candidate = os.path.abspath(os.path.join(root, filename))
 885                if candidate not in testdirs:
 886                    flist.append(candidate)
 887
 888    alltestcases = list()
 889    for casefile in flist:
 890        alltestcases = alltestcases + (load_from_file(casefile))
 891
 892    allcatlist = get_test_categories(alltestcases)
 893    allidlist = get_id_list(alltestcases)
 894
 895    testcases_by_cats = get_categorized_testlist(alltestcases, allcatlist)
 896    idtestcases = filter_tests_by_id(args, alltestcases)
 897    cattestcases = filter_tests_by_category(args, alltestcases)
 898
 899    cat_ids = [x['id'] for x in cattestcases]
 900    if args.execute:
 901        if args.category:
 902            alltestcases = cattestcases + [x for x in idtestcases if x['id'] not in cat_ids]
 903        else:
 904            alltestcases = idtestcases
 905    else:
 906        if cat_ids:
 907            alltestcases = cattestcases
 908        else:
 909            # just accept the existing value of alltestcases,
 910            # which has been filtered by file/directory
 911            pass
 912
 913    return allcatlist, allidlist, testcases_by_cats, alltestcases
 914
 915
 916def set_operation_mode(pm, parser, args, remaining):
 917    """
 918    Load the test case data and process remaining arguments to determine
 919    what the script should do for this run, and call the appropriate
 920    function.
 921    """
 922    ucat, idlist, testcases, alltests = get_test_cases(args)
 923
 924    if args.gen_id:
 925        if (has_blank_ids(idlist)):
 926            alltests = generate_case_ids(alltests)
 927        else:
 928            print("No empty ID fields found in test files.")
 929        exit(0)
 930
 931    duplicate_ids = check_case_id(alltests)
 932    if (len(duplicate_ids) > 0):
 933        print("The following test case IDs are not unique:")
 934        print(str(set(duplicate_ids)))
 935        print("Please correct them before continuing.")
 936        exit(1)
 937
 938    if args.showID:
 939        for atest in alltests:
 940            print_test_case(atest)
 941        exit(0)
 942
 943    if isinstance(args.category, list) and (len(args.category) == 0):
 944        print("Available categories:")
 945        print_sll(ucat)
 946        exit(0)
 947
 948    if args.list:
 949        list_test_cases(alltests)
 950        exit(0)
 951
 952    set_random(alltests)
 953
 954    exit_code = 0 # KSFT_PASS
 955    if len(alltests):
 956        req_plugins = pm.get_required_plugins(alltests)
 957        try:
 958            args = pm.load_required_plugins(req_plugins, parser, args, remaining)
 959        except PluginDependencyException as pde:
 960            print('The following plugins were not found:')
 961            print('{}'.format(pde.missing_pg))
 962
 963        if args.mp > 1:
 964            catresults = test_runner_mp(pm, args, alltests)
 965        else:
 966            catresults = test_runner_serial(pm, args, alltests)
 967
 968        if catresults.count_failures() != 0:
 969            exit_code = 1 # KSFT_FAIL
 970        if args.format == 'none':
 971            print('Test results output suppression requested\n')
 972        else:
 973            print('\nAll test results: \n')
 974            if args.format == 'xunit':
 975                suffix = 'xml'
 976                res = catresults.format_xunit()
 977            elif args.format == 'tap':
 978                suffix = 'tap'
 979                res = catresults.format_tap()
 980            print(res)
 981            print('\n\n')
 982            if not args.outfile:
 983                fname = 'test-results.{}'.format(suffix)
 984            else:
 985                fname = args.outfile
 986            with open(fname, 'w') as fh:
 987                fh.write(res)
 988                fh.close()
 989                if os.getenv('SUDO_UID') is not None:
 990                    os.chown(fname, uid=int(os.getenv('SUDO_UID')),
 991                        gid=int(os.getenv('SUDO_GID')))
 992    else:
 993        print('No tests found\n')
 994        exit_code = 4 # KSFT_SKIP
 995    exit(exit_code)
 996
 997def main():
 998    """
 999    Start of execution; set up argument parser and get the arguments,
1000    and start operations.
1001    """
1002    import resource
1003
1004    if sys.version_info.major < 3 or sys.version_info.minor < 8:
1005        sys.exit("tdc requires at least python 3.8")
1006
1007    resource.setrlimit(resource.RLIMIT_NOFILE, (1048576, 1048576))
1008
1009    parser = args_parse()
1010    parser = set_args(parser)
1011    pm = PluginMgr(parser)
1012    parser = pm.call_add_args(parser)
1013    (args, remaining) = parser.parse_known_args()
1014    args.NAMES = NAMES
1015    args.mp = min(args.mp, 4)
1016    pm.set_args(args)
1017    check_default_settings(args, remaining, pm)
1018    if args.verbose > 2:
1019        print('args is {}'.format(args))
1020
1021    try:
1022        set_operation_mode(pm, parser, args, remaining)
1023    except KeyboardInterrupt:
1024        # Cleanup on Ctrl-C
1025        pm.call_post_suite(None)
1026
1027if __name__ == "__main__":
1028    main()