Home | History | Annotate | Download | only in tools
      1 ## This file is part of Scapy
      2 ## See http://www.secdev.org/projects/scapy for more informations
      3 ## Copyright (C) Philippe Biondi <phil (at] secdev.org>
      4 ## This program is published under a GPLv2 license
      5 
      6 """
      7 Unit testing infrastructure for Scapy
      8 """
      9 
     10 from __future__ import absolute_import
     11 from __future__ import print_function
     12 import sys, getopt, imp, glob, importlib
     13 import hashlib, copy, bz2, base64, os.path, time, traceback, zlib
     14 from scapy.consts import WINDOWS
     15 import scapy.modules.six as six
     16 from scapy.modules.six.moves import range
     17 
     18 
     19 ### Util class ###
     20 
     21 class Bunch:
     22     __init__ = lambda self, **kw: setattr(self, '__dict__', kw)
     23 
     24 #### Import tool ####
     25 
     26 def import_module(name):
     27     name = os.path.realpath(name)
     28     thepath = os.path.dirname(name)
     29     name = os.path.basename(name)
     30     if name.endswith(".py"):
     31         name = name[:-3]
     32     f,path,desc = imp.find_module(name,[thepath])
     33     
     34     try:
     35         return imp.load_module(name, f, path, desc)
     36     finally:
     37         if f:
     38             f.close()
     39 
     40 
     41 #### INTERNAL/EXTERNAL FILE EMBEDDING ####
     42 
     43 class File:
     44     def __init__(self, name, URL, local):
     45         self.name = name
     46         self.local = local.encode("utf8")
     47         self.URL = URL
     48     def get_local(self):
     49         return bz2.decompress(base64.decodestring(self.local))
     50     def get_URL(self):
     51         return self.URL
     52     def write(self, dir):
     53         if dir:
     54             dir += "/"
     55         open(dir+self.name,"wb").write(self.get_local())
     56 
     57         
     58 # Embed a base64 encoded bziped version of js and css files
     59 # to work if you can't reach Internet.
     60 class External_Files:
     61     UTscapy_js = File("UTscapy.js", "http://www.secdev.org/projects/UTscapy/UTscapy.js",
     62 """QlpoOTFBWSZTWWVijKQAAXxfgERUYOvAChIhBAC/79+qQAH8AFA0poANAMjQAAAG
     63 ABo0NGEZNBo00BhgAaNDRhGTQaNNAYFURJinplGaKbRkJiekzSenqmpA0Gm1LFMp
     64 RUklVQlK9WUTZYpNFI1IiEWEFT09Sfj5uO+qO6S5DQwKIxM92+Zku94wL6V/1KTK
     65 an2c66Ug6SmVKy1ZIrgauxMVLF5xLH0lJRQuKlqLF10iatlTzqvw7S9eS3+h4lu3
     66 GZyMgoOude3NJ1pQy8eo+X96IYZw+ynehsiPj73m0rnvQ3QXZ9BJQiZQYQ5/uNcl
     67 2WOlC5vyQqV/BWsnr2NZYLYXQLDs/Bffk4ZfR4/SH6GfA5Xlek4xHNHqbSsRbREO
     68 gueXo3kcYi94K6hSO3ldD2O/qJXOFqJ8o3TE2aQahxtQpCVUKQMvODHwu2YkaORY
     69 ZC6gihEallcHDIAtRPScBACAJnUggYhLDX6DEko7nC9GvAw5OcEkiyDUbLdiGCzD
     70 aXWMC2DuQ2Y6sGf6NcRuON7QSbhHsPc4KKmZ/xdyRThQkGVijKQ=""")
     71     UTscapy_css = File("UTscapy.css","http://www.secdev.org/projects/UTscapy/UTscapy.css",
     72 """QlpoOTFBWSZTWTbBCNEAAE7fgHxwSB//+Cpj2QC//9/6UAR+63dxbNzO3ccmtGEk
     73 pM0m1I9E/Qp6g9Q09TNQ9QDR6gMgAkiBFG9U9TEGRkGgABoABoBmpJkRAaAxD1AN
     74 Gh6gNADQBzAATJgATCYJhDAEYAEiQkwIyJk0n6qenpqeoaMUeo9RgIxp6pX78kfx
     75 Jx4MUhDHKEb2pJAYAelG1cybiZBBDipH8ocxNyHDAqTUxiQmIAEDE3ApIBUUECAT
     76 7Lvlf4xA/sVK0QHkSlYtT0JmErdOjx1v5NONPYSjrIhQnbl1MbG5m+InMYmVAWJp
     77 uklD9cNdmQv2YigxbEtgUrsY2pDDV/qMT2SHnHsViu2rrp2LA01YJIHZqjYCGIQN
     78 sGNobFxAYHLqqMOj9TI2Y4GRpRCUGu82PnMnXUBgDSkTY4EfmygaqvUwbGMbPwyE
     79 220Q4G+sDvw7+6in3CAOS634pcOEAdREUW+QqMjvWvECrGISo1piv3vqubTGOL1c
     80 ssrFnnSfU4T6KSCbPs98HJ2yjWN4i8Bk5WrM/JmELLNeZ4vgMkA4JVQInNnWTUTe
     81 gmMSlJd/b7JuRwiM5RUzXOBTa0e3spO/rsNJiylu0rCxygdRo2koXdSJzmUVjJUm
     82 BOFIkUKq8LrE+oT9h2qUqqUQ25fGV7e7OFkpmZopqUi0WeIBzlXdYY0Zz+WUJUTC
     83 RC+CIPFIYh1RkopswMAop6ZjuZKRqR0WNuV+rfuF5aCXPpxAm0F14tPyhf42zFMT
     84 GJUMxxowJnoauRq4xGQk+2lYFxbQ0FiC43WZSyYLHMuo5NTJ92QLAgs4FgOyZQqQ
     85 xpsGKMA0cIisNeiootpnlWQvkPzNGUTPg8jqkwTvqQLguZLKJudha1hqfBib1IfO
     86 LNChcU6OqF+3wyPKg5Y5oSbSJPAMcRDANwmS2i9oZm6vsD1pLkWtFGbAkEjjCuEU
     87 W1ev1IsF2UVmWYFtJkqLT708ApUBK/ig3rbJWSq7RGQd3sSrOKu3lyKzTBdkXK2a
     88 BGLV5dS1XURdKxaRkMplLLQxsimBYZEAa8KQkYyI+4EagMqycRR7RgwtZFxJSu0T
     89 1q5wS2JG82iETHplbNj8DYo9IkmKzNAiw4FxK8bRfIYvwrbshbEagL11AQJFsqeZ
     90 WeXDoWEx2FMyyZRAB5QyCFnwYtwtWAQmmITY8aIM2SZyRnHH9Wi8+Sr2qyCscFYo
     91 vzM985aHXOHAxQN2UQZbQkUv3D4Vc+lyvalAffv3Tyg4ks3a22kPXiyeCGweviNX
     92 0K8TKasyOhGsVamTUAZBXfQVw1zmdS4rHDnbHgtIjX3DcCt6UIr0BHTYjdV0JbPj
     93 r1APYgXihjQwM2M83AKIhwQQJv/F3JFOFCQNsEI0QA==""")
     94     def get_local_dict(cls):
     95         return {x: y.name for (x, y) in six.iteritems(cls.__dict__)
     96                 if isinstance(y, File)}
     97     get_local_dict = classmethod(get_local_dict)
     98     def get_URL_dict(cls):
     99         return {x: y.URL for (x, y) in six.iteritems(cls.__dict__)
    100                 if isinstance(y, File)}
    101     get_URL_dict = classmethod(get_URL_dict)
    102 
    103 
    104 #### HELPER CLASSES FOR PARAMETRING OUTPUT FORMAT ####
    105 
    106 class EnumClass:
    107     def from_string(cls,x):
    108         return cls.__dict__[x.upper()]
    109     from_string = classmethod(from_string)
    110     
    111 class Format(EnumClass):
    112     TEXT  = 1
    113     ANSI  = 2
    114     HTML  = 3
    115     LATEX = 4
    116     XUNIT = 5
    117 
    118 
    119 #### TEST CLASSES ####
    120 
    121 class TestClass:
    122     def __getitem__(self, item):
    123         return getattr(self, item)
    124     def add_keywords(self, kws):
    125         if isinstance(kws, six.string_types):
    126             kws = [kws]
    127         for kwd in kws:
    128             if kwd.startswith('-'):
    129                 try:
    130                     self.keywords.remove(kwd[1:])
    131                 except KeyError:
    132                     pass
    133             else:
    134                 self.keywords.add(kwd)
    135 
    136 class TestCampaign(TestClass):
    137     def __init__(self, title):
    138         self.title = title
    139         self.filename = None
    140         self.headcomments = ""
    141         self.campaign = []
    142         self.keywords = set()
    143         self.crc = None
    144         self.sha = None
    145         self.preexec = None
    146         self.preexec_output = None
    147         self.end_pos = 0
    148     def add_testset(self, testset):
    149         self.campaign.append(testset)
    150         testset.keywords.update(self.keywords)
    151     def startNum(self, beginpos):
    152         for ts in self:
    153             for t in ts:
    154                 t.num = beginpos
    155                 beginpos += 1
    156         self.end_pos = beginpos
    157     def __iter__(self):
    158         return self.campaign.__iter__()
    159     def all_tests(self):
    160         for ts in self:
    161             for t in ts:
    162                 yield t
    163 
    164 class TestSet(TestClass):
    165     def __init__(self, name):
    166         self.name = name
    167         self.tests = []
    168         self.comments = ""
    169         self.keywords = set()
    170         self.crc = None
    171         self.expand = 1
    172     def add_test(self, test):
    173         self.tests.append(test)
    174         test.keywords.update(self.keywords)
    175     def __iter__(self):
    176         return self.tests.__iter__()
    177 
    178 class UnitTest(TestClass):
    179     def __init__(self, name):
    180         self.name = name
    181         self.test = ""
    182         self.comments = ""
    183         self.result = ""
    184         self.res = True  # must be True at init to have a different truth value than None
    185         self.output = ""
    186         self.num = -1
    187         self.keywords = set()
    188         self.crc = None
    189         self.expand = 1
    190     def decode(self):
    191         if six.PY2:
    192             self.test = self.test.decode("utf8", "ignore")
    193             self.output = self.output.decode("utf8", "ignore")
    194             self.comments = self.comments.decode("utf8", "ignore")
    195             self.result = self.result.decode("utf8", "ignore")
    196     def __nonzero__(self):
    197         return self.res
    198     __bool__ = __nonzero__
    199 
    200 
    201 # Careful note: all data not included will be set by default.
    202 # Use -c as first argument !!
    203 def parse_config_file(config_path, verb=3):
    204     """Parse provided json to get configuration
    205     Empty default json:
    206     {
    207       "testfiles": [],
    208       "onlyfailed": false,
    209       "verb": 2,
    210       "dump": 0,
    211       "crc": true,
    212       "scapy": "scapy",
    213       "preexec": {},
    214       "global_preexec": "",
    215       "outputfile": null,
    216       "local": true,
    217       "format": "ansi",
    218       "num": null,
    219       "modules": [],
    220       "kw_ok": [],
    221       "kw_ko": []
    222     }
    223 
    224     """
    225     import json, unicodedata
    226     with open(config_path) as config_file:
    227         data = json.load(config_file, encoding="utf8")
    228         if verb > 2:
    229             print("### Loaded config file", config_path, file=sys.stderr)
    230     def get_if_exist(key, default):
    231         return data[key] if key in data else default
    232     return Bunch(testfiles=get_if_exist("testfiles", []), onlyfailed=get_if_exist("onlyfailed", False),
    233                  verb=get_if_exist("verb", 3), dump=get_if_exist("dump", 0), crc=get_if_exist("crc", 1),
    234                  scapy=get_if_exist("scapy", "scapy"), preexec=get_if_exist("preexec", {}),
    235                  global_preexec=get_if_exist("global_preexec", ""), outfile=get_if_exist("outputfile", sys.stdout),
    236                  local=get_if_exist("local", 0), num=get_if_exist("num", None), modules=get_if_exist("modules", []),
    237                  kw_ok=get_if_exist("kw_ok", []), kw_ko=get_if_exist("kw_ko", []), format=get_if_exist("format", "ansi"))
    238 
    239 #### PARSE CAMPAIGN ####
    240 
    241 def parse_campaign_file(campaign_file):
    242     test_campaign = TestCampaign("Test campaign")
    243     test_campaign.filename=  campaign_file.name
    244     testset = None
    245     test = None
    246     testnb = 0
    247 
    248     for l in campaign_file.readlines():
    249         if l[0] == '#':
    250             continue
    251         if l[0] == "~":
    252             (test or testset or test_campaign).add_keywords(l[1:].split())
    253         elif l[0] == "%":
    254             test_campaign.title = l[1:].strip()
    255         elif l[0] == "+":
    256             testset = TestSet(l[1:].strip())
    257             test_campaign.add_testset(testset)
    258             test = None
    259         elif l[0] == "=":
    260             test = UnitTest(l[1:].strip())
    261             test.num = testnb
    262             testnb += 1
    263             testset.add_test(test)
    264         elif l[0] == "*":
    265             if test is not None:
    266                 test.comments += l[1:]
    267             elif testset is not None:
    268                 testset.comments += l[1:]
    269             else:
    270                 test_campaign.headcomments += l[1:]
    271         else:
    272             if test is None:
    273                 if l.strip():
    274                     print("Unknown content [%s]" % l.strip(), file=sys.stderr)
    275             else:
    276                 test.test += l
    277     return test_campaign
    278 
    279 def dump_campaign(test_campaign):
    280     print("#"*(len(test_campaign.title)+6))
    281     print("## %(title)s ##" % test_campaign)
    282     print("#"*(len(test_campaign.title)+6))
    283     if test_campaign.sha and test_campaign.crc:
    284         print("CRC=[%(crc)s] SHA=[%(sha)s]" % test_campaign)
    285     print("from file %(filename)s" % test_campaign)
    286     print()
    287     for ts in test_campaign:
    288         if ts.crc:
    289             print("+--[%s]%s(%s)--" % (ts.name,"-"*max(2,80-len(ts.name)-18),ts.crc))
    290         else:
    291             print("+--[%s]%s" % (ts.name,"-"*max(2,80-len(ts.name)-6)))
    292         if ts.keywords:
    293             print("  kw=%s" % ",".join(ts.keywords))
    294         for t in ts:
    295             print("%(num)03i %(name)s" % t)
    296             c = k = ""
    297             if t.keywords:
    298                 k = "kw=%s" % ",".join(t.keywords)
    299             if t.crc:
    300                 c = "[%(crc)s] " % t
    301             if c or k:
    302                 print("    %s%s" % (c,k)) 
    303 
    304 #### COMPUTE CAMPAIGN DIGESTS ####
    305 if six.PY2:
    306     def crc32(x):
    307         return "%08X" % (0xffffffff & zlib.crc32(x))
    308 
    309     def sha1(x):
    310          return hashlib.sha1(x).hexdigest().upper()
    311 else:
    312     def crc32(x):
    313         return "%08X" % (0xffffffff & zlib.crc32(bytearray(x, "utf8")))
    314 
    315     def sha1(x):
    316         return hashlib.sha1(x.encode("utf8")).hexdigest().upper()
    317 
    318 def compute_campaign_digests(test_campaign):
    319     dc = ""
    320     for ts in test_campaign:
    321         dts = ""
    322         for t in ts:
    323             dt = t.test.strip()
    324             t.crc = crc32(dt)
    325             dts += "\0"+dt
    326         ts.crc = crc32(dts)
    327         dc += "\0\x01"+dts
    328     test_campaign.crc = crc32(dc)
    329     test_campaign.sha = sha1(open(test_campaign.filename).read())
    330 
    331 
    332 #### FILTER CAMPAIGN #####
    333 
    334 def filter_tests_on_numbers(test_campaign, num):
    335     if num:
    336         for ts in test_campaign:
    337             ts.tests = [t for t in ts.tests if t.num in num]
    338         test_campaign.campaign = [ts for ts in test_campaign.campaign
    339                                   if ts.tests]
    340 
    341 def filter_tests_keep_on_keywords(test_campaign, kw):
    342     def kw_match(lst, kw):
    343         for k in lst:
    344             if k in kw:
    345                 return True
    346         return False
    347     
    348     if kw:
    349         for ts in test_campaign:
    350             ts.tests = [t for t in ts.tests if kw_match(t.keywords, kw)]
    351 
    352 def filter_tests_remove_on_keywords(test_campaign, kw):
    353     def kw_match(lst, kw):
    354         for k in kw:
    355             if k in lst:
    356                 return True
    357         return False
    358     
    359     if kw:
    360         for ts in test_campaign:
    361             ts.tests = [t for t in ts.tests if not kw_match(t.keywords, kw)]
    362 
    363 
    364 def remove_empty_testsets(test_campaign):
    365     test_campaign.campaign = [ts for ts in test_campaign.campaign if ts.tests]
    366 
    367 
    368 #### RUN CAMPAIGN #####
    369 
    370 def run_campaign(test_campaign, get_interactive_session, verb=3, ignore_globals=None):
    371     passed=failed=0
    372     if test_campaign.preexec:
    373         test_campaign.preexec_output = get_interactive_session(test_campaign.preexec.strip(), ignore_globals=ignore_globals)[0]
    374     for testset in test_campaign:
    375         for t in testset:
    376             t.output,res = get_interactive_session(t.test.strip(), ignore_globals=ignore_globals)
    377             the_res = False
    378             try:
    379                 if res is None or res:
    380                     the_res= True
    381             except Exception as msg:
    382                 t.output+="UTscapy: Error during result interpretation:\n"
    383                 t.output+="".join(traceback.format_exception(sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2],))
    384             if the_res:
    385                 t.res = True
    386                 res = "passed"
    387                 passed += 1
    388             else:
    389                 t.res = False
    390                 res = "failed"
    391                 failed += 1
    392             t.result = res
    393             t.decode()
    394             if verb > 1:
    395                 print("%(result)6s %(crc)s %(name)s" % t, file=sys.stderr)
    396     test_campaign.passed = passed
    397     test_campaign.failed = failed
    398     if verb:
    399         print("Campaign CRC=%(crc)s  SHA=%(sha)s" % test_campaign, file=sys.stderr)
    400         print("PASSED=%i FAILED=%i" % (passed, failed), file=sys.stderr)
    401     return failed
    402 
    403 
    404 #### INFO LINES ####
    405 
    406 def info_line(test_campaign):
    407     filename = test_campaign.filename
    408     if filename is None:
    409         return "Run %s by UTscapy" % time.ctime()
    410     else:
    411         return "Run %s from [%s] by UTscapy" % (time.ctime(), filename)
    412 
    413 def html_info_line(test_campaign):
    414     filename = test_campaign.filename
    415     if filename is None:
    416         return """Run %s by <a href="http://www.secdev.org/projects/UTscapy/">UTscapy</a><br>""" % time.ctime()
    417     else:
    418         return """Run %s from [%s] by <a href="http://www.secdev.org/projects/UTscapy/">UTscapy</a><br>""" % (time.ctime(), filename)
    419 
    420 
    421 #### CAMPAIGN TO something ####
    422 
    423 def campaign_to_TEXT(test_campaign):
    424     output="%(title)s\n" % test_campaign
    425     output += "-- "+info_line(test_campaign)+"\n\n"
    426     output += "Passed=%(passed)i\nFailed=%(failed)i\n\n%(headcomments)s\n" % test_campaign
    427     
    428     for testset in test_campaign:
    429         if any(t.expand for t in testset):
    430             output += "######\n## %(name)s\n######\n%(comments)s\n\n" % testset
    431             for t in testset:
    432                 if t.expand:
    433                     output += "###(%(num)03i)=[%(result)s] %(name)s\n%(comments)s\n%(output)s\n\n" % t
    434 
    435     return output
    436  
    437 def campaign_to_ANSI(test_campaign):
    438     output="%(title)s\n" % test_campaign
    439     output += "-- "+info_line(test_campaign)+"\n\n"
    440     output += "Passed=%(passed)i\nFailed=%(failed)i\n\n%(headcomments)s\n" % test_campaign
    441     
    442     for testset in test_campaign:
    443         if any(t.expand for t in testset):
    444             output += "######\n## %(name)s\n######\n%(comments)s\n\n" % testset
    445             for t in testset:
    446                 if t.expand:
    447                     output += "###(%(num)03i)=[%(result)s] %(name)s\n%(comments)s\n%(output)s\n\n" % t
    448 
    449     return output
    450 
    451 def campaign_to_xUNIT(test_campaign):
    452     output='<?xml version="1.0" encoding="UTF-8" ?>\n<testsuite>\n'
    453     for testset in test_campaign:
    454         for t in testset:
    455             output += ' <testcase classname="%s"\n' % testset.name.encode("string_escape").replace('"',' ')
    456             output += '           name="%s"\n' % t.name.encode("string_escape").replace('"',' ')
    457             output += '           duration="0">\n' % t
    458             if not t.res:
    459                 output += '<error><![CDATA[%(output)s]]></error>\n' % t
    460             output += "</testcase>\n"
    461     output += '</testsuite>'
    462     return output
    463 
    464 
    465 def campaign_to_HTML(test_campaign):
    466     output = """
    467 <h1>%(title)s</h1>
    468 
    469 <p>
    470 """ % test_campaign
    471 
    472     if test_campaign.crc is not None and test_campaign.sha is not None:
    473         output += "CRC=<span class=crc>%(crc)s</span> SHA=<span class=crc>%(sha)s</span><br>" % test_campaign
    474     output += "<small><em>"+html_info_line(test_campaign)+"</em></small>"
    475     output += test_campaign.headcomments +  "\n<p>PASSED=%(passed)i FAILED=%(failed)i<p>\n\n" % test_campaign
    476 
    477     for testset in test_campaign:
    478         output += "<h2>" % testset
    479         if testset.crc is not None:
    480             output += "<span class=crc>%(crc)s</span> " % testset
    481         output += "%(name)s</h2>\n%(comments)s\n<ul>\n" % testset
    482         for t in testset:
    483             output += """<li class=%(result)s id="tst%(num)il">\n""" % t
    484             if t.expand == 2:
    485                 output +="""
    486 <span id="tst%(num)i+" class="button%(result)s" onClick="show('tst%(num)i')" style="POSITION: absolute; VISIBILITY: hidden;">+%(num)03i+</span>
    487 <span id="tst%(num)i-" class="button%(result)s" onClick="hide('tst%(num)i')">-%(num)03i-</span>
    488 """ % t
    489             else:
    490                 output += """
    491 <span id="tst%(num)i+" class="button%(result)s" onClick="show('tst%(num)i')">+%(num)03i+</span>
    492 <span id="tst%(num)i-" class="button%(result)s" onClick="hide('tst%(num)i')" style="POSITION: absolute; VISIBILITY: hidden;">-%(num)03i-</span>
    493 """ % t
    494             if t.crc is not None:
    495                 output += "<span class=crc>%(crc)s</span>\n" % t
    496             output += """%(name)s\n<span class="comment %(result)s" id="tst%(num)i" """ % t
    497             if t.expand < 2:
    498                 output += """ style="POSITION: absolute; VISIBILITY: hidden;" """
    499             output += """><br>%(comments)s
    500 <pre>
    501 %(output)s</pre></span>
    502 """ % t
    503         output += "\n</ul>\n\n"
    504     return output
    505 
    506 def pack_html_campaigns(runned_campaigns, data, local=0, title=None):
    507     output = """
    508 <html>
    509 <head>
    510 <title>%(title)s</title>
    511 <h1>UTScapy tests</h1>
    512 
    513 <span class=button onClick="hide_all('tst')">Shrink All</span>
    514 <span class=button onClick="show_all('tst')">Expand All</span>
    515 <span class=button onClick="show_passed('tst')">Expand Passed</span>
    516 <span class=button onClick="show_failed('tst')">Expand Failed</span>
    517 
    518 <p>
    519 """
    520     for test_campaign in runned_campaigns:
    521         for ts in test_campaign:
    522             for t in ts:
    523                 output += """<span class=button%(result)s onClick="goto_id('tst%(num)il')">%(num)03i</span>\n""" % t
    524         
    525     output += """</p>\n\n
    526 <link rel="stylesheet" href="%(UTscapy_css)s" type="text/css">
    527 <script language="JavaScript" src="%(UTscapy_js)s" type="text/javascript"></script>
    528 </head>
    529 <body>
    530 %(data)s
    531 </body></html>
    532 """
    533     out_dict = {'data': data, 'title': title if title else "UTScapy tests"}
    534     if local:
    535         External_Files.UTscapy_js.write(os.path.dirname(test_campaign.output_file.name))
    536         External_Files.UTscapy_css.write(os.path.dirname(test_campaign.output_file.name))
    537         out_dict.update(External_Files.get_local_dict())
    538     else:
    539         out_dict.update(External_Files.get_URL_dict())
    540 
    541     output %= out_dict
    542     return output
    543 
    544 def campaign_to_LATEX(test_campaign):
    545     output = r"""\documentclass{report}
    546 \usepackage{alltt}
    547 \usepackage{xcolor}
    548 \usepackage{a4wide}
    549 \usepackage{hyperref}
    550 
    551 \title{%(title)s}
    552 \date{%%s}
    553 
    554 \begin{document}
    555 \maketitle
    556 \tableofcontents
    557 
    558 \begin{description}
    559 \item[Passed:] %(passed)i
    560 \item[Failed:] %(failed)i
    561 \end{description}
    562 
    563 %(headcomments)s
    564 
    565 """ % test_campaign
    566     output %= info_line(test_campaign)
    567     
    568     for testset in test_campaign:
    569         output += "\\chapter{%(name)s}\n\n%(comments)s\n\n" % testset
    570         for t in testset:
    571             if t.expand:
    572                 output += r"""\section{%(name)s}
    573             
    574 [%(num)03i] [%(result)s]
    575 
    576 %(comments)s
    577 \begin{alltt}
    578 %(output)s
    579 \end{alltt}
    580 
    581 """ % t
    582 
    583     output += "\\end{document}\n"
    584     return output
    585 
    586 
    587 
    588 #### USAGE ####
    589                       
    590 def usage():
    591     print("""Usage: UTscapy [-m module] [-f {text|ansi|HTML|LaTeX}] [-o output_file] 
    592                [-t testfile] [-T testfile] [-k keywords [-k ...]] [-K keywords [-K ...]]
    593                [-l] [-d|-D] [-F] [-q[q]] [-P preexecute_python_code]
    594                [-s /path/to/scapy] [-c configfile]
    595 -t\t\t: provide test files (can be used many times)
    596 -T\t\t: if -t is used with *, remove a specific file (can be used many times)
    597 -l\t\t: generate local files
    598 -F\t\t: expand only failed tests
    599 -d\t\t: dump campaign
    600 -D\t\t: dump campaign and stop
    601 -C\t\t: don't calculate CRC and SHA
    602 -s\t\t: path to scapy.py
    603 -c\t\t: load a .utsc config file
    604 -q\t\t: quiet mode
    605 -qq\t\t: [silent mode]
    606 -n <testnum>\t: only tests whose numbers are given (eg. 1,3-7,12)
    607 -m <module>\t: additional module to put in the namespace
    608 -k <kw1>,<kw2>,...\t: include only tests with one of those keywords (can be used many times)
    609 -K <kw1>,<kw2>,...\t: remove tests with one of those keywords (can be used many times)
    610 -P <preexecute_python_code>
    611 """, file=sys.stderr)
    612     raise SystemExit
    613 
    614 
    615 #### MAIN ####
    616 
    617 def execute_campaign(TESTFILE, OUTPUTFILE, PREEXEC, NUM, KW_OK, KW_KO, DUMP,
    618                      FORMAT, VERB, ONLYFAILED, CRC, autorun_func, pos_begin=0, ignore_globals=None):
    619     # Parse test file
    620     test_campaign = parse_campaign_file(TESTFILE)
    621 
    622     # Report parameters
    623     if PREEXEC:
    624         test_campaign.preexec = PREEXEC
    625     
    626     # Compute campaign CRC and SHA
    627     if CRC:
    628         compute_campaign_digests(test_campaign)
    629 
    630     # Filter out unwanted tests
    631     filter_tests_on_numbers(test_campaign, NUM)
    632     for k in KW_OK:
    633         filter_tests_keep_on_keywords(test_campaign, k)
    634     for k in KW_KO:
    635         filter_tests_remove_on_keywords(test_campaign, k)
    636 
    637     remove_empty_testsets(test_campaign)
    638 
    639 
    640     # Dump campaign
    641     if DUMP:
    642         dump_campaign(test_campaign)
    643         if DUMP > 1:
    644             sys.exit()
    645 
    646     # Run tests
    647     test_campaign.output_file = OUTPUTFILE
    648     result = run_campaign(test_campaign, autorun_func[FORMAT], verb=VERB, ignore_globals=None)
    649 
    650     # Shrink passed
    651     if ONLYFAILED:
    652         for t in test_campaign.all_tests():
    653             if t:
    654                 t.expand = 0
    655             else:
    656                 t.expand = 2
    657 
    658     pos_end = 0
    659     # Generate report
    660     if FORMAT == Format.TEXT:
    661         output = campaign_to_TEXT(test_campaign)
    662     elif FORMAT == Format.ANSI:
    663         output = campaign_to_ANSI(test_campaign)
    664     elif FORMAT == Format.HTML:
    665         test_campaign.startNum(pos_begin)
    666         output = campaign_to_HTML(test_campaign)
    667     elif FORMAT == Format.LATEX:
    668         output = campaign_to_LATEX(test_campaign)
    669     elif FORMAT == Format.XUNIT:
    670         output = campaign_to_xUNIT(test_campaign)
    671 
    672     return output, (result == 0), test_campaign
    673 
    674 def resolve_testfiles(TESTFILES):
    675     for tfile in TESTFILES[:]:
    676         if "*" in tfile:
    677             TESTFILES.remove(tfile)
    678             TESTFILES.extend(glob.glob(tfile))
    679     return TESTFILES
    680 
    681 def main(argv):
    682     ignore_globals = list(six.moves.builtins.__dict__.keys())
    683 
    684     # Parse arguments
    685     
    686     FORMAT = Format.ANSI
    687     TESTFILE = sys.stdin
    688     OUTPUTFILE = sys.stdout
    689     LOCAL = 0
    690     NUM = None
    691     KW_OK = []
    692     KW_KO = []
    693     DUMP = 0
    694     CRC = True
    695     ONLYFAILED = False
    696     VERB = 3
    697     GLOB_PREEXEC = ""
    698     PREEXEC_DICT = {}
    699     SCAPY = "scapy"
    700     MODULES = []
    701     TESTFILES = []
    702     try:
    703         opts = getopt.getopt(argv, "o:t:T:c:f:hln:m:k:K:DdCFqP:s:")
    704         for opt,optarg in opts[0]:
    705             if opt == "-h":
    706                 usage()
    707             elif opt == "-F":
    708                 ONLYFAILED = True
    709             elif opt == "-q":
    710                 VERB -= 1
    711             elif opt == "-D":
    712                 DUMP = 2
    713             elif opt == "-d":
    714                 DUMP = 1
    715             elif opt == "-C":
    716                 CRC = False
    717             elif opt == "-s":
    718                 SCAPY = optarg
    719             elif opt == "-P":
    720                 GLOB_PREEXEC += "\n"+optarg
    721             elif opt == "-f":
    722                 try:
    723                     FORMAT = Format.from_string(optarg)
    724                 except KeyError as msg:
    725                     raise getopt.GetoptError("Unknown output format %s" % msg)
    726             elif opt == "-t":
    727                 TESTFILES.append(optarg)
    728                 TESTFILES = resolve_testfiles(TESTFILES)
    729             elif opt == "-T":
    730                 TESTFILES.remove(optarg)
    731             elif opt == "-c":
    732                 data = parse_config_file(optarg, VERB)
    733                 ONLYFAILED = data.onlyfailed
    734                 VERB = data.verb
    735                 DUMP = data.dump
    736                 CRC = data.crc
    737                 SCAPY = data.scapy
    738                 PREEXEC_DICT = data.preexec
    739                 GLOB_PREEXEC = data.global_preexec
    740                 OUTPUTFILE = data.outfile
    741                 TESTFILES = data.testfiles
    742                 LOCAL = 1 if data.local else 0
    743                 NUM = data.num
    744                 MODULES = data.modules
    745                 KW_OK = [data.kw_ok]
    746                 KW_KO = [data.kw_ko]
    747                 try:
    748                     FORMAT = Format.from_string(data.format)
    749                 except KeyError as msg:
    750                     raise getopt.GetoptError("Unknown output format %s" % msg)
    751                 TESTFILES = resolve_testfiles(TESTFILES)
    752             elif opt == "-o":
    753                 OUTPUTFILE = open(optarg, "wb")
    754             elif opt == "-l":
    755                 LOCAL = 1
    756             elif opt == "-n":
    757                 NUM = []
    758                 for v in (x.strip() for x in optarg.split(",")):
    759                     try:
    760                         NUM.append(int(v))
    761                     except ValueError:
    762                         v1, v2 = [int(e) for e in v.split('-', 1)]
    763                         NUM.extend(range(v1, v2 + 1))
    764             elif opt == "-m":
    765                 MODULES.append(optarg)
    766             elif opt == "-k":
    767                 KW_OK.append(optarg.split(","))
    768             elif opt == "-K":
    769                 KW_KO.append(optarg.split(","))
    770 
    771         if VERB > 2:
    772             print("### Booting scapy...", file=sys.stderr)
    773         try:
    774             from scapy import all as scapy
    775         except ImportError as e:
    776             raise getopt.GetoptError("cannot import [%s]: %s" % (SCAPY,e))
    777 
    778         for m in MODULES:
    779             try:
    780                 mod = import_module(m)
    781                 six.moves.builtins.__dict__.update(mod.__dict__)
    782             except ImportError as e:
    783                 raise getopt.GetoptError("cannot import [%s]: %s" % (m,e))
    784                 
    785     except getopt.GetoptError as msg:
    786         print("ERROR:",msg, file=sys.stderr)
    787         raise SystemExit
    788 
    789     autorun_func = {
    790         Format.TEXT: scapy.autorun_get_text_interactive_session,
    791         Format.ANSI: scapy.autorun_get_ansi_interactive_session,
    792         Format.HTML: scapy.autorun_get_html_interactive_session,
    793         Format.LATEX: scapy.autorun_get_latex_interactive_session,
    794         Format.XUNIT: scapy.autorun_get_text_interactive_session,
    795         }
    796 
    797     if VERB > 2:
    798         print("### Starting tests...", file=sys.stderr)
    799 
    800     glob_output = ""
    801     glob_result = 0
    802     glob_title = None
    803 
    804     UNIQUE = len(TESTFILES) == 1
    805 
    806     # Resolve tags and asterix
    807     for prex in six.iterkeys(copy.copy(PREEXEC_DICT)):
    808         if "*" in prex:
    809             pycode = PREEXEC_DICT[prex]
    810             del PREEXEC_DICT[prex]
    811             for gl in glob.iglob(prex):
    812                 _pycode = pycode.replace("%name%", os.path.splitext(os.path.split(gl)[1])[0])
    813                 PREEXEC_DICT[gl] = _pycode
    814 
    815     pos_begin = 0
    816 
    817     runned_campaigns = []
    818     # Execute all files
    819     for TESTFILE in TESTFILES:
    820         if VERB > 2:
    821             print("### Loading:", TESTFILE, file=sys.stderr)
    822         PREEXEC = PREEXEC_DICT[TESTFILE] if TESTFILE in PREEXEC_DICT else GLOB_PREEXEC
    823         output, result, campaign = execute_campaign(open(TESTFILE), OUTPUTFILE,
    824                                           PREEXEC, NUM, KW_OK, KW_KO,
    825                                           DUMP, FORMAT, VERB, ONLYFAILED,
    826                                           CRC, autorun_func, pos_begin, ignore_globals)
    827         runned_campaigns.append(campaign)
    828         pos_begin = campaign.end_pos
    829         if UNIQUE:
    830             glob_title = campaign.title
    831         glob_output += output
    832         if not result:
    833             glob_result = 1
    834             break
    835 
    836     if VERB > 2:
    837             print("### Writing output...", file=sys.stderr)
    838     # Concenate outputs
    839     if FORMAT == Format.HTML:
    840         glob_output = pack_html_campaigns(runned_campaigns, glob_output, LOCAL, glob_title)
    841     
    842     OUTPUTFILE.write(glob_output.encode("utf8", "ignore")
    843                      if 'b' in OUTPUTFILE.mode else glob_output)
    844     OUTPUTFILE.close()
    845 
    846     # Return state
    847     return glob_result
    848 
    849 if __name__ == "__main__":
    850     sys.exit(main(sys.argv[1:]))
    851