BFD: improve ip header handling, fix assert
[vpp.git] / test / framework.py
1 #!/usr/bin/env python
2
3 import subprocess
4 import unittest
5 import tempfile
6 import time
7 import resource
8 from time import sleep
9 from Queue import Queue
10 from threading import Thread
11 from inspect import getdoc
12 from hook import StepHook, PollHook
13 from vpp_pg_interface import VppPGInterface
14 from vpp_lo_interface import VppLoInterface
15 from vpp_papi_provider import VppPapiProvider
16 from scapy.packet import Raw
17 from log import *
18
19 """
20   Test framework module.
21
22   The module provides a set of tools for constructing and running tests and
23   representing the results.
24 """
25
26
27 class _PacketInfo(object):
28     """Private class to create packet info object.
29
30     Help process information about the next packet.
31     Set variables to default values.
32     """
33     #: Store the index of the packet.
34     index = -1
35     #: Store the index of the source packet generator interface of the packet.
36     src = -1
37     #: Store the index of the destination packet generator interface
38     #: of the packet.
39     dst = -1
40     #: Store the copy of the former packet.
41     data = None
42
43
44 def pump_output(out, queue):
45     for line in iter(out.readline, b''):
46         queue.put(line)
47
48
49 class VppTestCase(unittest.TestCase):
50     """This subclass is a base class for VPP test cases that are implemented as
51     classes. It provides methods to create and run test case.
52     """
53
54     @property
55     def packet_infos(self):
56         """List of packet infos"""
57         return self._packet_infos
58
59     @packet_infos.setter
60     def packet_infos(self, value):
61         self._packet_infos = value
62
63     @classmethod
64     def instance(cls):
65         """Return the instance of this testcase"""
66         return cls.test_instance
67
68     @classmethod
69     def set_debug_flags(cls, d):
70         cls.debug_core = False
71         cls.debug_gdb = False
72         cls.debug_gdbserver = False
73         if d is None:
74             return
75         dl = d.lower()
76         if dl == "core":
77             if resource.getrlimit(resource.RLIMIT_CORE)[0] <= 0:
78                 # give a heads up if this is actually useless
79                 cls.logger.critical("WARNING: core size limit is set 0, core "
80                                     "files will NOT be created")
81             cls.debug_core = True
82         elif dl == "gdb":
83             cls.debug_gdb = True
84         elif dl == "gdbserver":
85             cls.debug_gdbserver = True
86         else:
87             raise Exception("Unrecognized DEBUG option: '%s'" % d)
88
89     @classmethod
90     def setUpConstants(cls):
91         """ Set-up the test case class based on environment variables """
92         try:
93             s = os.getenv("STEP")
94             cls.step = True if s.lower() in ("y", "yes", "1") else False
95         except:
96             cls.step = False
97         try:
98             d = os.getenv("DEBUG")
99         except:
100             d = None
101         cls.set_debug_flags(d)
102         cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp")
103         cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH')
104         debug_cli = ""
105         if cls.step or cls.debug_gdb or cls.debug_gdbserver:
106             debug_cli = "cli-listen localhost:5002"
107         cls.vpp_cmdline = [cls.vpp_bin, "unix", "{", "nodaemon", debug_cli, "}",
108                            "api-segment", "{", "prefix", cls.shm_prefix, "}"]
109         if cls.plugin_path is not None:
110             cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path])
111         cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline)
112
113     @classmethod
114     def wait_for_enter(cls):
115         if cls.debug_gdbserver:
116             print(double_line_delim)
117             print("Spawned GDB server with PID: %d" % cls.vpp.pid)
118         elif cls.debug_gdb:
119             print(double_line_delim)
120             print("Spawned VPP with PID: %d" % cls.vpp.pid)
121         else:
122             cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid)
123             return
124         print(single_line_delim)
125         print("You can debug the VPP using e.g.:")
126         if cls.debug_gdbserver:
127             print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'")
128             print("Now is the time to attach a gdb by running the above "
129                   "command, set up breakpoints etc. and then resume VPP from "
130                   "within gdb by issuing the 'continue' command")
131         elif cls.debug_gdb:
132             print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid)
133             print("Now is the time to attach a gdb by running the above "
134                   "command and set up breakpoints etc.")
135         print(single_line_delim)
136         raw_input("Press ENTER to continue running the testcase...")
137
138     @classmethod
139     def run_vpp(cls):
140         cmdline = cls.vpp_cmdline
141
142         if cls.debug_gdbserver:
143             gdbserver = '/usr/bin/gdbserver'
144             if not os.path.isfile(gdbserver) or \
145                     not os.access(gdbserver, os.X_OK):
146                 raise Exception("gdbserver binary '%s' does not exist or is "
147                                 "not executable" % gdbserver)
148
149             cmdline = [gdbserver, 'localhost:7777'] + cls.vpp_cmdline
150             cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline))
151
152         try:
153             cls.vpp = subprocess.Popen(cmdline,
154                                        stdout=subprocess.PIPE,
155                                        stderr=subprocess.PIPE,
156                                        bufsize=1)
157         except Exception as e:
158             cls.logger.critical("Couldn't start vpp: %s" % e)
159             raise
160
161         cls.wait_for_enter()
162
163     @classmethod
164     def setUpClass(cls):
165         """
166         Perform class setup before running the testcase
167         Remove shared memory files, start vpp and connect the vpp-api
168         """
169         cls.logger = getLogger(cls.__name__)
170         cls.tempdir = tempfile.mkdtemp(
171             prefix='vpp-unittest-' + cls.__name__ + '-')
172         cls.shm_prefix = cls.tempdir.split("/")[-1]
173         os.chdir(cls.tempdir)
174         cls.logger.info("Temporary dir is %s, shm prefix is %s",
175                         cls.tempdir, cls.shm_prefix)
176         cls.setUpConstants()
177         cls.pg_streams = []
178         cls.packet_infos = {}
179         cls.verbose = 0
180         cls.vpp_dead = False
181         print(double_line_delim)
182         print(colorize(getdoc(cls).splitlines()[0], YELLOW))
183         print(double_line_delim)
184         # need to catch exceptions here because if we raise, then the cleanup
185         # doesn't get called and we might end with a zombie vpp
186         try:
187             cls.run_vpp()
188             cls.vpp_stdout_queue = Queue()
189             cls.vpp_stdout_reader_thread = Thread(target=pump_output, args=(
190                 cls.vpp.stdout, cls.vpp_stdout_queue))
191             cls.vpp_stdout_reader_thread.start()
192             cls.vpp_stderr_queue = Queue()
193             cls.vpp_stderr_reader_thread = Thread(target=pump_output, args=(
194                 cls.vpp.stderr, cls.vpp_stderr_queue))
195             cls.vpp_stderr_reader_thread.start()
196             cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix, cls)
197             if cls.step:
198                 hook = StepHook(cls)
199             else:
200                 hook = PollHook(cls)
201             cls.vapi.register_hook(hook)
202             time.sleep(0.1)
203             hook.poll_vpp()
204             try:
205                 cls.vapi.connect()
206             except:
207                 if cls.debug_gdbserver:
208                     print(colorize("You're running VPP inside gdbserver but "
209                                    "VPP-API connection failed, did you forget "
210                                    "to 'continue' VPP from within gdb?", RED))
211                 raise
212         except:
213             t, v, tb = sys.exc_info()
214             try:
215                 cls.quit()
216             except:
217                 pass
218             raise t, v, tb
219
220     @classmethod
221     def quit(cls):
222         """
223         Disconnect vpp-api, kill vpp and cleanup shared memory files
224         """
225         if (cls.debug_gdbserver or cls.debug_gdb) and hasattr(cls, 'vpp'):
226             cls.vpp.poll()
227             if cls.vpp.returncode is None:
228                 print(double_line_delim)
229                 print("VPP or GDB server is still running")
230                 print(single_line_delim)
231                 raw_input("When done debugging, press ENTER to kill the process"
232                           " and finish running the testcase...")
233
234         if hasattr(cls, 'vpp'):
235             if hasattr(cls, 'vapi'):
236                 cls.vapi.disconnect()
237             cls.vpp.poll()
238             if cls.vpp.returncode is None:
239                 cls.vpp.terminate()
240             del cls.vpp
241
242         if hasattr(cls, 'vpp_stdout_queue'):
243             cls.logger.info(single_line_delim)
244             cls.logger.info('VPP output to stdout while running %s:',
245                             cls.__name__)
246             cls.logger.info(single_line_delim)
247             f = open(cls.tempdir + '/vpp_stdout.txt', 'w')
248             while not cls.vpp_stdout_queue.empty():
249                 line = cls.vpp_stdout_queue.get_nowait()
250                 f.write(line)
251                 cls.logger.info('VPP stdout: %s' % line.rstrip('\n'))
252
253         if hasattr(cls, 'vpp_stderr_queue'):
254             cls.logger.info(single_line_delim)
255             cls.logger.info('VPP output to stderr while running %s:',
256                             cls.__name__)
257             cls.logger.info(single_line_delim)
258             f = open(cls.tempdir + '/vpp_stderr.txt', 'w')
259             while not cls.vpp_stderr_queue.empty():
260                 line = cls.vpp_stderr_queue.get_nowait()
261                 f.write(line)
262                 cls.logger.info('VPP stderr: %s' % line.rstrip('\n'))
263             cls.logger.info(single_line_delim)
264
265     @classmethod
266     def tearDownClass(cls):
267         """ Perform final cleanup after running all tests in this test-case """
268         cls.quit()
269
270     def tearDown(self):
271         """ Show various debug prints after each test """
272         if not self.vpp_dead:
273             self.logger.debug(self.vapi.cli("show trace"))
274             self.logger.info(self.vapi.ppcli("show int"))
275             self.logger.info(self.vapi.ppcli("show hardware"))
276             self.logger.info(self.vapi.ppcli("show error"))
277             self.logger.info(self.vapi.ppcli("show run"))
278
279     def setUp(self):
280         """ Clear trace before running each test"""
281         if self.vpp_dead:
282             raise Exception("VPP is dead when setting up the test")
283         self.vapi.cli("clear trace")
284         # store the test instance inside the test class - so that objects
285         # holding the class can access instance methods (like assertEqual)
286         type(self).test_instance = self
287
288     @classmethod
289     def pg_enable_capture(cls, interfaces):
290         """
291         Enable capture on packet-generator interfaces
292
293         :param interfaces: iterable interface indexes
294
295         """
296         for i in interfaces:
297             i.enable_capture()
298
299     @classmethod
300     def pg_start(cls):
301         """
302         Enable the packet-generator and send all prepared packet streams
303         Remove the packet streams afterwards
304         """
305         cls.vapi.cli("trace add pg-input 50")  # 50 is maximum
306         cls.vapi.cli('packet-generator enable')
307         sleep(1)  # give VPP some time to process the packets
308         for stream in cls.pg_streams:
309             cls.vapi.cli('packet-generator delete %s' % stream)
310         cls.pg_streams = []
311
312     @classmethod
313     def create_pg_interfaces(cls, interfaces):
314         """
315         Create packet-generator interfaces
316
317         :param interfaces: iterable indexes of the interfaces
318
319         """
320         result = []
321         for i in interfaces:
322             intf = VppPGInterface(cls, i)
323             setattr(cls, intf.name, intf)
324             result.append(intf)
325         cls.pg_interfaces = result
326         return result
327
328     @classmethod
329     def create_loopback_interfaces(cls, interfaces):
330         """
331         Create loopback interfaces
332
333         :param interfaces: iterable indexes of the interfaces
334
335         """
336         result = []
337         for i in interfaces:
338             intf = VppLoInterface(cls, i)
339             setattr(cls, intf.name, intf)
340             result.append(intf)
341         cls.lo_interfaces = result
342         return result
343
344     @staticmethod
345     def extend_packet(packet, size):
346         """
347         Extend packet to given size by padding with spaces
348         NOTE: Currently works only when Raw layer is present.
349
350         :param packet: packet
351         :param size: target size
352
353         """
354         packet_len = len(packet) + 4
355         extend = size - packet_len
356         if extend > 0:
357             packet[Raw].load += ' ' * extend
358
359     def add_packet_info_to_list(self, info):
360         """
361         Add packet info to the testcase's packet info list
362
363         :param info: packet info
364
365         """
366         info.index = len(self.packet_infos)
367         self.packet_infos[info.index] = info
368
369     def create_packet_info(self, src_pg_index, dst_pg_index):
370         """
371         Create packet info object containing the source and destination indexes
372         and add it to the testcase's packet info list
373
374         :param src_pg_index: source packet-generator index
375         :param dst_pg_index: destination packet-generator index
376
377         :returns: _PacketInfo object
378
379         """
380         info = _PacketInfo()
381         self.add_packet_info_to_list(info)
382         info.src = src_pg_index
383         info.dst = dst_pg_index
384         return info
385
386     @staticmethod
387     def info_to_payload(info):
388         """
389         Convert _PacketInfo object to packet payload
390
391         :param info: _PacketInfo object
392
393         :returns: string containing serialized data from packet info
394         """
395         return "%d %d %d" % (info.index, info.src, info.dst)
396
397     @staticmethod
398     def payload_to_info(payload):
399         """
400         Convert packet payload to _PacketInfo object
401
402         :param payload: packet payload
403
404         :returns: _PacketInfo object containing de-serialized data from payload
405
406         """
407         numbers = payload.split()
408         info = _PacketInfo()
409         info.index = int(numbers[0])
410         info.src = int(numbers[1])
411         info.dst = int(numbers[2])
412         return info
413
414     def get_next_packet_info(self, info):
415         """
416         Iterate over the packet info list stored in the testcase
417         Start iteration with first element if info is None
418         Continue based on index in info if info is specified
419
420         :param info: info or None
421         :returns: next info in list or None if no more infos
422         """
423         if info is None:
424             next_index = 0
425         else:
426             next_index = info.index + 1
427         if next_index == len(self.packet_infos):
428             return None
429         else:
430             return self.packet_infos[next_index]
431
432     def get_next_packet_info_for_interface(self, src_index, info):
433         """
434         Search the packet info list for the next packet info with same source
435         interface index
436
437         :param src_index: source interface index to search for
438         :param info: packet info - where to start the search
439         :returns: packet info or None
440
441         """
442         while True:
443             info = self.get_next_packet_info(info)
444             if info is None:
445                 return None
446             if info.src == src_index:
447                 return info
448
449     def get_next_packet_info_for_interface2(self, src_index, dst_index, info):
450         """
451         Search the packet info list for the next packet info with same source
452         and destination interface indexes
453
454         :param src_index: source interface index to search for
455         :param dst_index: destination interface index to search for
456         :param info: packet info - where to start the search
457         :returns: packet info or None
458
459         """
460         while True:
461             info = self.get_next_packet_info_for_interface(src_index, info)
462             if info is None:
463                 return None
464             if info.dst == dst_index:
465                 return info
466
467     def assert_equal(self, real_value, expected_value, name_or_class=None):
468         if name_or_class is None:
469             self.assertEqual(real_value, expected_value, msg)
470             return
471         try:
472             msg = "Invalid %s: %d('%s') does not match expected value %d('%s')"
473             msg = msg % (getdoc(name_or_class).strip(),
474                          real_value, str(name_or_class(real_value)),
475                          expected_value, str(name_or_class(expected_value)))
476         except:
477             msg = "Invalid %s: %s does not match expected value %s" % (
478                 name_or_class, real_value, expected_value)
479
480         self.assertEqual(real_value, expected_value, msg)
481
482     def assert_in_range(
483             self,
484             real_value,
485             expected_min,
486             expected_max,
487             name=None):
488         if name is None:
489             msg = None
490         else:
491             msg = "Invalid %s: %s out of range <%s,%s>" % (
492                 name, real_value, expected_min, expected_max)
493         self.assertTrue(expected_min <= real_value <= expected_max, msg)
494
495
496 class VppTestResult(unittest.TestResult):
497     """
498     @property result_string
499      String variable to store the test case result string.
500     @property errors
501      List variable containing 2-tuples of TestCase instances and strings
502      holding formatted tracebacks. Each tuple represents a test which
503      raised an unexpected exception.
504     @property failures
505      List variable containing 2-tuples of TestCase instances and strings
506      holding formatted tracebacks. Each tuple represents a test where
507      a failure was explicitly signalled using the TestCase.assert*()
508      methods.
509     """
510
511     def __init__(self, stream, descriptions, verbosity):
512         """
513         :param stream File descriptor to store where to report test results. Set
514             to the standard error stream by default.
515         :param descriptions Boolean variable to store information if to use test
516             case descriptions.
517         :param verbosity Integer variable to store required verbosity level.
518         """
519         unittest.TestResult.__init__(self, stream, descriptions, verbosity)
520         self.stream = stream
521         self.descriptions = descriptions
522         self.verbosity = verbosity
523         self.result_string = None
524
525     def addSuccess(self, test):
526         """
527         Record a test succeeded result
528
529         :param test:
530
531         """
532         unittest.TestResult.addSuccess(self, test)
533         self.result_string = colorize("OK", GREEN)
534
535     def addSkip(self, test, reason):
536         """
537         Record a test skipped.
538
539         :param test:
540         :param reason:
541
542         """
543         unittest.TestResult.addSkip(self, test, reason)
544         self.result_string = colorize("SKIP", YELLOW)
545
546     def addFailure(self, test, err):
547         """
548         Record a test failed result
549
550         :param test:
551         :param err: error message
552
553         """
554         unittest.TestResult.addFailure(self, test, err)
555         if hasattr(test, 'tempdir'):
556             self.result_string = colorize("FAIL", RED) + \
557                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
558         else:
559             self.result_string = colorize("FAIL", RED) + ' [no temp dir]'
560
561     def addError(self, test, err):
562         """
563         Record a test error result
564
565         :param test:
566         :param err: error message
567
568         """
569         unittest.TestResult.addError(self, test, err)
570         if hasattr(test, 'tempdir'):
571             self.result_string = colorize("ERROR", RED) + \
572                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
573         else:
574             self.result_string = colorize("ERROR", RED) + ' [no temp dir]'
575
576     def getDescription(self, test):
577         """
578         Get test description
579
580         :param test:
581         :returns: test description
582
583         """
584         # TODO: if none print warning not raise exception
585         short_description = test.shortDescription()
586         if self.descriptions and short_description:
587             return short_description
588         else:
589             return str(test)
590
591     def startTest(self, test):
592         """
593         Start a test
594
595         :param test:
596
597         """
598         unittest.TestResult.startTest(self, test)
599         if self.verbosity > 0:
600             self.stream.writeln(
601                 "Starting " + self.getDescription(test) + " ...")
602             self.stream.writeln(single_line_delim)
603
604     def stopTest(self, test):
605         """
606         Stop a test
607
608         :param test:
609
610         """
611         unittest.TestResult.stopTest(self, test)
612         if self.verbosity > 0:
613             self.stream.writeln(single_line_delim)
614             self.stream.writeln("%-60s%s" %
615                                 (self.getDescription(test), self.result_string))
616             self.stream.writeln(single_line_delim)
617         else:
618             self.stream.writeln("%-60s%s" %
619                                 (self.getDescription(test), self.result_string))
620
621     def printErrors(self):
622         """
623         Print errors from running the test case
624         """
625         self.stream.writeln()
626         self.printErrorList('ERROR', self.errors)
627         self.printErrorList('FAIL', self.failures)
628
629     def printErrorList(self, flavour, errors):
630         """
631         Print error list to the output stream together with error type
632         and test case description.
633
634         :param flavour: error type
635         :param errors: iterable errors
636
637         """
638         for test, err in errors:
639             self.stream.writeln(double_line_delim)
640             self.stream.writeln("%s: %s" %
641                                 (flavour, self.getDescription(test)))
642             self.stream.writeln(single_line_delim)
643             self.stream.writeln("%s" % err)
644
645
646 class VppTestRunner(unittest.TextTestRunner):
647     """
648     A basic test runner implementation which prints results on standard error.
649     """
650     @property
651     def resultclass(self):
652         """Class maintaining the results of the tests"""
653         return VppTestResult
654
655     def run(self, test):
656         """
657         Run the tests
658
659         :param test:
660
661         """
662         print("Running tests using custom test runner")  # debug message
663         return super(VppTestRunner, self).run(test)