1## This file is part of Scapy
2## See http://www.secdev.org/projects/scapy for more informations
3## Copyright (C) Philippe Biondi <phil@secdev.org>
4## This program is published under a GPLv2 license
5
6"""
7Unit testing infrastructure for Scapy
8"""
9
10import sys,getopt,imp
11import bz2, base64, os.path, time, traceback, zlib, sha
12
13
14#### Import tool ####
15
16def import_module(name):
17    name = os.path.realpath(name)
18    thepath = os.path.dirname(name)
19    name = os.path.basename(name)
20    if name.endswith(".py"):
21        name = name[:-3]
22    f,path,desc = imp.find_module(name,[thepath])
23
24    try:
25        return imp.load_module(name, f, path, desc)
26    finally:
27        if f:
28            f.close()
29
30
31#### INTERNAL/EXTERNAL FILE EMBEDDING ####
32
33class File:
34    def __init__(self, name, URL, local):
35        self.name = name
36        self.local = local
37        self.URL = URL
38    def get_local(self):
39        return bz2.decompress(base64.decodestring(self.local))
40    def get_URL(self):
41        return URL
42    def write(self, dir):
43        if dir:
44            dir += "/"
45        open(dir+self.name,"w").write(self.get_local())
46
47
48# Embed a base64 encoded bziped version of js and css files
49# to work if you can't reach Internet.
50class External_Files:
51    UTscapy_js = File("UTscapy.js", "http://www.secdev.org/projects/UTscapy/UTscapy.js",
52"""QlpoOTFBWSZTWWVijKQAAXxfgERUYOvAChIhBAC/79+qQAH8AFA0poANAMjQAAAG
53ABo0NGEZNBo00BhgAaNDRhGTQaNNAYFURJinplGaKbRkJiekzSenqmpA0Gm1LFMp
54RUklVQlK9WUTZYpNFI1IiEWEFT09Sfj5uO+qO6S5DQwKIxM92+Zku94wL6V/1KTK
55an2c66Ug6SmVKy1ZIrgauxMVLF5xLH0lJRQuKlqLF10iatlTzqvw7S9eS3+h4lu3
56GZyMgoOude3NJ1pQy8eo+X96IYZw+ynehsiPj73m0rnvQ3QXZ9BJQiZQYQ5/uNcl
572WOlC5vyQqV/BWsnr2NZYLYXQLDs/Bffk4ZfR4/SH6GfA5Xlek4xHNHqbSsRbREO
58gueXo3kcYi94K6hSO3ldD2O/qJXOFqJ8o3TE2aQahxtQpCVUKQMvODHwu2YkaORY
59ZC6gihEallcHDIAtRPScBACAJnUggYhLDX6DEko7nC9GvAw5OcEkiyDUbLdiGCzD
60aXWMC2DuQ2Y6sGf6NcRuON7QSbhHsPc4KKmZ/xdyRThQkGVijKQ=""")
61    UTscapy_css = File("UTscapy.css","http://www.secdev.org/projects/UTscapy/UTscapy.css",
62"""QlpoOTFBWSZTWTbBCNEAAE7fgHxwSB//+Cpj2QC//9/6UAR+63dxbNzO3ccmtGEk
63pM0m1I9E/Qp6g9Q09TNQ9QDR6gMgAkiBFG9U9TEGRkGgABoABoBmpJkRAaAxD1AN
64Gh6gNADQBzAATJgATCYJhDAEYAEiQkwIyJk0n6qenpqeoaMUeo9RgIxp6pX78kfx
65Jx4MUhDHKEb2pJAYAelG1cybiZBBDipH8ocxNyHDAqTUxiQmIAEDE3ApIBUUECAT
667Lvlf4xA/sVK0QHkSlYtT0JmErdOjx1v5NONPYSjrIhQnbl1MbG5m+InMYmVAWJp
67uklD9cNdmQv2YigxbEtgUrsY2pDDV/qMT2SHnHsViu2rrp2LA01YJIHZqjYCGIQN
68sGNobFxAYHLqqMOj9TI2Y4GRpRCUGu82PnMnXUBgDSkTY4EfmygaqvUwbGMbPwyE
69220Q4G+sDvw7+6in3CAOS634pcOEAdREUW+QqMjvWvECrGISo1piv3vqubTGOL1c
70ssrFnnSfU4T6KSCbPs98HJ2yjWN4i8Bk5WrM/JmELLNeZ4vgMkA4JVQInNnWTUTe
71gmMSlJd/b7JuRwiM5RUzXOBTa0e3spO/rsNJiylu0rCxygdRo2koXdSJzmUVjJUm
72BOFIkUKq8LrE+oT9h2qUqqUQ25fGV7e7OFkpmZopqUi0WeIBzlXdYY0Zz+WUJUTC
73RC+CIPFIYh1RkopswMAop6ZjuZKRqR0WNuV+rfuF5aCXPpxAm0F14tPyhf42zFMT
74GJUMxxowJnoauRq4xGQk+2lYFxbQ0FiC43WZSyYLHMuo5NTJ92QLAgs4FgOyZQqQ
75xpsGKMA0cIisNeiootpnlWQvkPzNGUTPg8jqkwTvqQLguZLKJudha1hqfBib1IfO
76LNChcU6OqF+3wyPKg5Y5oSbSJPAMcRDANwmS2i9oZm6vsD1pLkWtFGbAkEjjCuEU
77W1ev1IsF2UVmWYFtJkqLT708ApUBK/ig3rbJWSq7RGQd3sSrOKu3lyKzTBdkXK2a
78BGLV5dS1XURdKxaRkMplLLQxsimBYZEAa8KQkYyI+4EagMqycRR7RgwtZFxJSu0T
791q5wS2JG82iETHplbNj8DYo9IkmKzNAiw4FxK8bRfIYvwrbshbEagL11AQJFsqeZ
80WeXDoWEx2FMyyZRAB5QyCFnwYtwtWAQmmITY8aIM2SZyRnHH9Wi8+Sr2qyCscFYo
81vzM985aHXOHAxQN2UQZbQkUv3D4Vc+lyvalAffv3Tyg4ks3a22kPXiyeCGweviNX
820K8TKasyOhGsVamTUAZBXfQVw1zmdS4rHDnbHgtIjX3DcCt6UIr0BHTYjdV0JbPj
83r1APYgXihjQwM2M83AKIhwQQJv/F3JFOFCQNsEI0QA==""")
84    def get_local_dict(cls):
85        return dict(map(lambda (x,y): (x, y.name),  filter(lambda (x,y): isinstance(y, File), cls.__dict__.items())))
86    get_local_dict = classmethod(get_local_dict)
87    def get_URL_dict(cls):
88        return dict(map(lambda (x,y): (x, y.URL),  filter(lambda (x,y): isinstance(y, File), cls.__dict__.items())))
89    get_URL_dict = classmethod(get_URL_dict)
90
91
92#### HELPER CLASSES FOR PARAMETRING OUTPUT FORMAT ####
93
94class EnumClass:
95    def from_string(cls,x):
96        return cls.__dict__[x.upper()]
97    from_string = classmethod(from_string)
98
99class Format(EnumClass):
100    TEXT  = 1
101    ANSI  = 2
102    HTML  = 3
103    LATEX = 4
104    XUNIT = 5
105
106
107#### TEST CLASSES ####
108
109class TestClass:
110    def __getitem__(self, item):
111        return getattr(self, item)
112    def add_keywords(self, kw):
113        if kw is str:
114            self.keywords.append(kw)
115        else:
116            self.keywords += kw
117
118class TestCampaign(TestClass):
119    def __init__(self, title):
120        self.title = title
121        self.filename = None
122        self.headcomments = ""
123        self.campaign = []
124        self.keywords = []
125        self.crc = None
126        self.sha = None
127        self.preexec = None
128        self.preexec_output = None
129    def add_testset(self, testset):
130        self.campaign.append(testset)
131    def __iter__(self):
132        return self.campaign.__iter__()
133    def all_tests(self):
134        for ts in self:
135            for t in ts:
136                yield t
137
138class TestSet(TestClass):
139    def __init__(self, name):
140        self.name = name
141        self.set = []
142        self.comments = ""
143        self.keywords = []
144        self.crc = None
145        self.expand = 1
146    def add_test(self, test):
147        self.set.append(test)
148    def __iter__(self):
149        return self.set.__iter__()
150
151class UnitTest(TestClass):
152    def __init__(self, name):
153        self.name = name
154        self.test = ""
155        self.comments = ""
156        self.result = ""
157        self.res = True  # must be True at init to have a different truth value than None
158        self.output = ""
159        self.num = -1
160        self.keywords = []
161        self.crc = None
162        self.expand = 1
163    def __nonzero__(self):
164        return self.res
165
166
167#### PARSE CAMPAIGN ####
168
169def parse_campaign_file(campaign_file):
170    test_campaign = TestCampaign("Test campaign")
171    test_campaign.filename=  campaign_file.name
172    testset = None
173    test = None
174    testnb = 0
175
176    for l in campaign_file.readlines():
177        if l[0] == '#':
178            continue
179        if l[0] == "~":
180            (test or testset or campaign_file).add_keywords(l[1:].split())
181        elif l[0] == "%":
182            test_campaign.title = l[1:].strip()
183        elif l[0] == "+":
184            testset = TestSet(l[1:].strip())
185            test_campaign.add_testset(testset)
186            test = None
187        elif l[0] == "=":
188            test = UnitTest(l[1:].strip())
189            test.num = testnb
190            testnb += 1
191            testset.add_test(test)
192        elif l[0] == "*":
193            if test is not None:
194
195                test.comments += l[1:]
196            elif testset is not None:
197                testset.comments += l[1:]
198            else:
199                test_campaign.headcomments += l[1:]
200        else:
201            if test is None:
202                if l.strip():
203                    print >>sys.stderr, "Unkonwn content [%s]" % l.strip()
204            else:
205                test.test += l
206    return test_campaign
207
208def dump_campaign(test_campaign):
209    print "#"*(len(test_campaign.title)+6)
210    print "## %(title)s ##" % test_campaign
211    print "#"*(len(test_campaign.title)+6)
212    if test_campaign.sha and test_campaign.crc:
213        print "CRC=[%(crc)s] SHA=[%(sha)s]" % test_campaign
214    print "from file %(filename)s" % test_campaign
215    print
216    for ts in test_campaign:
217        if ts.crc:
218            print "+--[%s]%s(%s)--" % (ts.name,"-"*max(2,80-len(ts.name)-18),ts.crc)
219        else:
220            print "+--[%s]%s" % (ts.name,"-"*max(2,80-len(ts.name)-6))
221        if ts.keywords:
222            print "  kw=%s" % ",".join(ts.keywords)
223        for t in ts:
224            print "%(num)03i %(name)s" % t
225            c = k = ""
226            if t.keywords:
227                k = "kw=%s" % ",".join(t.keywords)
228            if t.crc:
229                c = "[%(crc)s] " % t
230            if c or k:
231                print "    %s%s" % (c,k)
232
233#### COMPUTE CAMPAIGN DIGESTS ####
234
235def crc32(x):
236    return "%08X" % (0xffffffffL & zlib.crc32(x))
237
238def sha1(x):
239    return sha.sha(x).hexdigest().upper()
240
241def compute_campaign_digests(test_campaign):
242    dc = ""
243    for ts in test_campaign:
244        dts = ""
245        for t in ts:
246            dt = t.test.strip()
247            t.crc = crc32(dt)
248            dts += "\0"+dt
249        ts.crc = crc32(dts)
250        dc += "\0\x01"+dts
251    test_campaign.crc = crc32(dc)
252    test_campaign.sha = sha1(open(test_campaign.filename).read())
253
254
255#### FILTER CAMPAIGN #####
256
257def filter_tests_on_numbers(test_campaign, num):
258    if num:
259        for ts in test_campaign:
260            ts.set = filter(lambda t: t.num in num, ts.set)
261        test_campaign.campaign = filter(lambda ts: len(ts.set) > 0, test_campaign.campaign)
262
263def filter_tests_keep_on_keywords(test_campaign, kw):
264    def kw_match(lst, kw):
265        for k in lst:
266            if k in kw:
267                return True
268        return False
269
270    if kw:
271        for ts in test_campaign:
272            ts.set = filter(lambda t: kw_match(t.keywords, kw), ts.set)
273
274def filter_tests_remove_on_keywords(test_campaign, kw):
275    def kw_match(lst, kw):
276        for k in kw:
277            if k not in lst:
278                return False
279        return True
280
281    if kw:
282        for ts in test_campaign:
283            ts.set = filter(lambda t: not kw_match(t.keywords, kw), ts.set)
284
285
286def remove_empty_testsets(test_campaign):
287    test_campaign.campaign = filter(lambda ts: len(ts.set) > 0, test_campaign.campaign)
288
289
290#### RUN CAMPAIGN #####
291
292def run_campaign(test_campaign, get_interactive_session, verb=2):
293    passed=failed=0
294    if test_campaign.preexec:
295        test_campaign.preexec_output = get_interactive_session(test_campaign.preexec.strip())[0]
296    for testset in test_campaign:
297        for t in testset:
298            t.output,res = get_interactive_session(t.test.strip())
299            the_res = False
300            try:
301                if res is None or res:
302                    the_res= True
303            except Exception,msg:
304                t.output+="UTscapy: Error during result interpretation:\n"
305                t.output+="".join(traceback.format_exception(sys.exc_type, sys.exc_value, sys.exc_traceback,))
306            if the_res:
307                t.res = True
308                res = "passed"
309                passed += 1
310            else:
311                t.res = False
312                res = "failed"
313                failed += 1
314            t.result = res
315            if verb > 1:
316                print >>sys.stderr,"%(result)6s %(crc)s %(name)s" % t
317    test_campaign.passed = passed
318    test_campaign.failed = failed
319    if verb:
320        print >>sys.stderr,"Campaign CRC=%(crc)s  SHA=%(sha)s" % test_campaign
321        print >>sys.stderr,"PASSED=%i FAILED=%i" % (passed, failed)
322
323
324#### INFO LINES ####
325
326def info_line(test_campaign):
327    filename = test_campaign.filename
328    if filename is None:
329        return "Run %s by UTscapy" % time.ctime()
330    else:
331        return "Run %s from [%s] by UTscapy" % (time.ctime(), filename)
332
333def html_info_line(test_campaign):
334    filename = test_campaign.filename
335    if filename is None:
336        return """Run %s by <a href="http://www.secdev.org/projects/UTscapy/">UTscapy</a><br>""" % time.ctime()
337    else:
338        return """Run %s from [%s] by <a href="http://www.secdev.org/projects/UTscapy/">UTscapy</a><br>""" % (time.ctime(), filename)
339
340
341#### CAMPAIGN TO something ####
342
343def campaign_to_TEXT(test_campaign):
344    output="%(title)s\n" % test_campaign
345    output += "-- "+info_line(test_campaign)+"\n\n"
346    output += "Passed=%(passed)i\nFailed=%(failed)i\n\n%(headcomments)s\n" % test_campaign
347
348    for testset in test_campaign:
349        output += "######\n## %(name)s\n######\n%(comments)s\n\n" % testset
350        for t in testset:
351            if t.expand:
352                output += "###(%(num)03i)=[%(result)s] %(name)s\n%(comments)s\n%(output)s\n\n" % t
353
354    return output
355
356def campaign_to_ANSI(test_campaign):
357    output="%(title)s\n" % test_campaign
358    output += "-- "+info_line(test_campaign)+"\n\n"
359    output += "Passed=%(passed)i\nFailed=%(failed)i\n\n%(headcomments)s\n" % test_campaign
360
361    for testset in test_campaign:
362        output += "######\n## %(name)s\n######\n%(comments)s\n\n" % testset
363        for t in testset:
364            if t.expand:
365                output += "###(%(num)03i)=[%(result)s] %(name)s\n%(comments)s\n%(output)s\n\n" % t
366
367    return output
368
369def campaign_to_xUNIT(test_campaign):
370    output='<?xml version="1.0" encoding="UTF-8" ?>\n<testsuite>\n'
371    for testset in test_campaign:
372        for t in testset:
373            output += ' <testcase classname="%s"\n' % testset.name.encode("string_escape").replace('"',' ')
374            output += '           name="%s"\n' % t.name.encode("string_escape").replace('"',' ')
375            output += '           duration="0">\n' % t
376            if not t.res:
377                output += '<error><![CDATA[%(output)s]]></error>\n' % t
378            output += "</testcase>\n"
379    output += '</testsuite>'
380    return output
381
382
383def campaign_to_HTML(test_campaign, local=0):
384    output = """<html>
385<head>
386<title>%(title)s</title>
387<link rel="stylesheet" href="%%(UTscapy_css)s" type="text/css">
388<script language="JavaScript" src="%%(UTscapy_js)s" type="text/javascript"></script>
389</head>
390<body>
391
392<h1>%(title)s</h1>
393
394<span class=button onClick="hide_all('tst')">Shrink All</span>
395<span class=button onClick="show_all('tst')">Expand All</span>
396<span class=button onClick="show_passed('tst')">Expand Passed</span>
397<span class=button onClick="show_failed('tst')">Expand Failed</span>
398<p>
399""" % test_campaign
400
401    if local:
402        External_Files.UTscapy_js.write(os.path.dirname(test_campaign.output_file.name))
403        External_Files.UTscapy_css.write(os.path.dirname(test_campaign.output_file.name))
404        output %= External_Files.get_local_dict()
405    else:
406        output %= External_Files.get_URL_dict()
407
408    if test_campaign.crc is not None and test_campaign.sha is not None:
409        output += "CRC=<span class=crc>%(crc)s</span> SHA=<span class=crc>%(sha)s</span><br>" % test_campaign
410    output += "<small><em>"+html_info_line(test_campaign)+"</em></small>"
411    output += test_campaign.headcomments +  "\n<p>PASSED=%(passed)i FAILED=%(failed)i<p>\n\n" % test_campaign
412    for ts in test_campaign:
413        for t in ts:
414            output += """<span class=button%(result)s onClick="goto_id('tst%(num)il')">%(num)03i</span>\n""" % t
415    output += "\n\n"
416
417    for testset in test_campaign:
418        output += "<h2>" % testset
419        if testset.crc is not None:
420            output += "<span class=crc>%(crc)s</span> " % testset
421        output += "%(name)s</h2>\n%(comments)s\n<ul>\n" % testset
422        for t in testset:
423            output += """<li class=%(result)s id="tst%(num)il">\n""" % t
424            if t.expand == 2:
425                output +="""
426<span id="tst%(num)i+" class="button%(result)s" onClick="show('tst%(num)i')" style="POSITION: absolute; VISIBILITY: hidden;">+%(num)03i+</span>
427<span id="tst%(num)i-" class="button%(result)s" onClick="hide('tst%(num)i')">-%(num)03i-</span>
428""" % t
429            else:
430                output += """
431<span id="tst%(num)i+" class="button%(result)s" onClick="show('tst%(num)i')">+%(num)03i+</span>
432<span id="tst%(num)i-" class="button%(result)s" onClick="hide('tst%(num)i')" style="POSITION: absolute; VISIBILITY: hidden;">-%(num)03i-</span>
433""" % t
434            if t.crc is not None:
435                output += "<span class=crc>%(crc)s</span>\n" % t
436            output += """%(name)s\n<span class="comment %(result)s" id="tst%(num)i" """ % t
437            if t.expand < 2:
438                output += """ style="POSITION: absolute; VISIBILITY: hidden;" """
439            output += """><br>%(comments)s
440<pre>
441%(output)s</pre></span>
442""" % t
443        output += "\n</ul>\n\n"
444
445    output += "</body></html>"
446    return output
447
448def campaign_to_LATEX(test_campaign):
449    output = r"""\documentclass{report}
450\usepackage{alltt}
451\usepackage{xcolor}
452\usepackage{a4wide}
453\usepackage{hyperref}
454
455\title{%(title)s}
456\date{%%s}
457
458\begin{document}
459\maketitle
460\tableofcontents
461
462\begin{description}
463\item[Passed:] %(passed)i
464\item[Failed:] %(failed)i
465\end{description}
466
467%(headcomments)s
468
469""" % test_campaign
470    output %= info_line(test_campaign)
471
472    for testset in test_campaign:
473        output += "\\chapter{%(name)s}\n\n%(comments)s\n\n" % testset
474        for t in testset:
475            if t.expand:
476                output += r"""\section{%(name)s}
477
478[%(num)03i] [%(result)s]
479
480%(comments)s
481\begin{alltt}
482%(output)s
483\end{alltt}
484
485""" % t
486
487    output += "\\end{document}\n"
488    return output
489
490
491
492#### USAGE ####
493
494def usage():
495    print >>sys.stderr,"""Usage: UTscapy [-m module] [-f {text|ansi|HTML|LaTeX}] [-o output_file]
496               [-t testfile] [-k keywords [-k ...]] [-K keywords [-K ...]]
497               [-l] [-d|-D] [-F] [-q[q]] [-P preexecute_python_code]
498               [-s /path/to/scpay]
499-l\t\t: generate local files
500-F\t\t: expand only failed tests
501-d\t\t: dump campaign
502-D\t\t: dump campaign and stop
503-C\t\t: don't calculate CRC and SHA
504-s\t\t: path to scapy.py
505-q\t\t: quiet mode
506-qq\t\t: [silent mode]
507-n <testnum>\t: only tests whose numbers are given (eg. 1,3-7,12)
508-m <module>\t: additional module to put in the namespace
509-k <kw1>,<kw2>,...\t: include only tests with one of those keywords (can be used many times)
510-K <kw1>,<kw2>,...\t: remove tests with one of those keywords (can be used many times)
511-P <preexecute_python_code>
512"""
513    raise SystemExit
514
515
516#### MAIN ####
517
518def main(argv):
519    import __builtin__
520
521    # Parse arguments
522
523    FORMAT = Format.ANSI
524    TESTFILE = sys.stdin
525    OUTPUTFILE = sys.stdout
526    LOCAL = 0
527    NUM=None
528    KW_OK = []
529    KW_KO = []
530    DUMP = 0
531    CRC = 1
532    ONLYFAILED = 0
533    VERB=2
534    PREEXEC=""
535    SCAPY="scapy"
536    MODULES = []
537    try:
538        opts = getopt.getopt(argv, "o:t:f:hln:m:k:K:DdCFqP:s:")
539        for opt,optarg in opts[0]:
540            if opt == "-h":
541                usage()
542            elif opt == "-F":
543                ONLYFAILED = 1
544            elif opt == "-q":
545                VERB -= 1
546            elif opt == "-D":
547                DUMP = 2
548            elif opt == "-d":
549                DUMP = 1
550            elif opt == "-C":
551                CRC = 0
552            elif opt == "-s":
553                SCAPY = optarg
554            elif opt == "-P":
555                PREEXEC += "\n"+optarg
556            elif opt == "-f":
557                try:
558                    FORMAT = Format.from_string(optarg)
559                except KeyError,msg:
560                    raise getopt.GetoptError("Unknown output format %s" % msg)
561            elif opt == "-t":
562                TESTFILE = open(optarg)
563            elif opt == "-o":
564                OUTPUTFILE = open(optarg, "w")
565            elif opt == "-l":
566                LOCAL = 1
567            elif opt == "-n":
568                NUM = []
569                for v in map( lambda x: x.strip(), optarg.split(",") ):
570                    try:
571                        NUM.append(int(v))
572                    except ValueError:
573                        v1,v2 = map(int, v.split("-"))
574                        for vv in range(v1,v2+1):
575                            NUM.append(vv)
576            elif opt == "-m":
577                MODULES.append(optarg)
578            elif opt == "-k":
579                KW_OK.append(optarg.split(","))
580            elif opt == "-K":
581                KW_KO.append(optarg.split(","))
582
583
584        try:
585            from scapy import all as scapy
586        except ImportError,e:
587            raise getopt.GetoptError("cannot import [%s]: %s" % (SCAPY,e))
588
589        for m in MODULES:
590            try:
591                mod = import_module(m)
592                __builtin__.__dict__.update(mod.__dict__)
593            except ImportError,e:
594                raise getopt.GetoptError("cannot import [%s]: %s" % (m,e))
595
596    except getopt.GetoptError,msg:
597        print >>sys.stderr,"ERROR:",msg
598        raise SystemExit
599
600    autorun_func = {
601        Format.TEXT: scapy.autorun_get_text_interactive_session,
602        Format.ANSI: scapy.autorun_get_ansi_interactive_session,
603        Format.HTML: scapy.autorun_get_html_interactive_session,
604        Format.LATEX: scapy.autorun_get_latex_interactive_session,
605        Format.XUNIT: scapy.autorun_get_text_interactive_session,
606        }
607
608    # Parse test file
609    test_campaign = parse_campaign_file(TESTFILE)
610
611    # Report parameters
612    if PREEXEC:
613        test_campaign.preexec = PREEXEC
614
615
616    # Compute campaign CRC and SHA
617    if CRC:
618        compute_campaign_digests(test_campaign)
619
620    # Filter out unwanted tests
621    filter_tests_on_numbers(test_campaign, NUM)
622    for k in KW_OK:
623        filter_tests_keep_on_keywords(test_campaign, k)
624    for k in KW_KO:
625        filter_tests_remove_on_keywords(test_campaign, k)
626
627    remove_empty_testsets(test_campaign)
628
629
630    # Dump campaign
631    if DUMP:
632        dump_campaign(test_campaign)
633        if DUMP > 1:
634            sys.exit()
635
636    # Run tests
637    test_campaign.output_file = OUTPUTFILE
638    run_campaign(test_campaign, autorun_func[FORMAT], verb=VERB)
639
640    # Shrink passed
641    if ONLYFAILED:
642        for t in test_campaign.all_tests():
643            if t:
644                t.expand = 0
645            else:
646                t.expand = 2
647
648    # Generate report
649    if FORMAT == Format.TEXT:
650        output = campaign_to_TEXT(test_campaign)
651    elif FORMAT == Format.ANSI:
652        output = campaign_to_ANSI(test_campaign)
653    elif FORMAT == Format.HTML:
654        output = campaign_to_HTML(test_campaign, local=LOCAL)
655    elif FORMAT == Format.LATEX:
656        output = campaign_to_LATEX(test_campaign)
657    elif FORMAT == Format.XUNIT:
658        output = campaign_to_xUNIT(test_campaign)
659
660    OUTPUTFILE.write(output)
661    OUTPUTFILE.close()
662
663if __name__ == "__main__":
664    main(sys.argv[1:])
665