xref: /OK3568_Linux_fs/kernel/tools/testing/selftests/tc-testing/tdc.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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