BFD: fix timing in the main loop
[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 collections import deque
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, deque):
45     for line in iter(out.readline, b''):
46         deque.append(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_deque = deque()
189             cls.vpp_stdout_reader_thread = Thread(target=pump_output, args=(
190                 cls.vpp.stdout, cls.vpp_stdout_deque))
191             cls.vpp_stdout_reader_thread.start()
192             cls.vpp_stderr_deque = deque()
193             cls.vpp_stderr_reader_thread = Thread(target=pump_output, args=(
194                 cls.vpp.stderr, cls.vpp_stderr_deque))
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_deque'):
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             vpp_output = "".join(cls.vpp_stdout_deque)
249             f.write(vpp_output)
250             cls.logger.info('\n%s', vpp_output)
251             cls.logger.info(single_line_delim)
252
253         if hasattr(cls, 'vpp_stderr_deque'):
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             vpp_output = "".join(cls.vpp_stderr_deque)
260             f.write(vpp_output)
261             cls.logger.info('\n%s', vpp_output)
262             cls.logger.info(single_line_delim)
263
264     @classmethod
265     def tearDownClass(cls):
266         """ Perform final cleanup after running all tests in this test-case """
267         cls.quit()
268
269     def tearDown(self):
270         """ Show various debug prints after each test """
271         if not self.vpp_dead:
272             self.logger.debug(self.vapi.cli("show trace"))
273             self.logger.info(self.vapi.ppcli("show int"))
274             self.logger.info(self.vapi.ppcli("show hardware"))
275             self.logger.info(self.vapi.ppcli("show error"))
276             self.logger.info(self.vapi.ppcli("show run"))
277
278     def setUp(self):
279         """ Clear trace before running each test"""
280         if self.vpp_dead:
281             raise Exception("VPP is dead when setting up the test")
282         time.sleep(.1)
283         self.vpp_stdout_deque.append(
284             "--- test setUp() for %s.%s(%s) starts here ---\n" %
285             (self.__class__.__name__, self._testMethodName,
286              self._testMethodDoc))
287         self.vpp_stderr_deque.append(
288             "--- test setUp() for %s.%s(%s) starts here ---\n" %
289             (self.__class__.__name__, self._testMethodName,
290              self._testMethodDoc))
291         self.vapi.cli("clear trace")
292         # store the test instance inside the test class - so that objects
293         # holding the class can access instance methods (like assertEqual)
294         type(self).test_instance = self
295
296     @classmethod
297     def pg_enable_capture(cls, interfaces):
298         """
299         Enable capture on packet-generator interfaces
300
301         :param interfaces: iterable interface indexes
302
303         """
304         for i in interfaces:
305             i.enable_capture()
306
307     @classmethod
308     def pg_start(cls, sleep_time=1):
309         """
310         Enable the packet-generator and send all prepared packet streams
311         Remove the packet streams afterwards
312         """
313         cls.vapi.cli("trace add pg-input 50")  # 50 is maximum
314         cls.vapi.cli('packet-generator enable')
315         sleep(sleep_time)  # give VPP some time to process the packets
316         for stream in cls.pg_streams:
317             cls.vapi.cli('packet-generator delete %s' % stream)
318         cls.pg_streams = []
319
320     @classmethod
321     def create_pg_interfaces(cls, interfaces):
322         """
323         Create packet-generator interfaces
324
325         :param interfaces: iterable indexes of the interfaces
326
327         """
328         result = []
329         for i in interfaces:
330             intf = VppPGInterface(cls, i)
331             setattr(cls, intf.name, intf)
332             result.append(intf)
333         cls.pg_interfaces = result
334         return result
335
336     @classmethod
337     def create_loopback_interfaces(cls, interfaces):
338         """
339         Create loopback interfaces
340
341         :param interfaces: iterable indexes of the interfaces
342
343         """
344         result = []
345         for i in interfaces:
346             intf = VppLoInterface(cls, i)
347             setattr(cls, intf.name, intf)
348             result.append(intf)
349         cls.lo_interfaces = result
350         return result
351
352     @staticmethod
353     def extend_packet(packet, size):
354         """
355         Extend packet to given size by padding with spaces
356         NOTE: Currently works only when Raw layer is present.
357
358         :param packet: packet
359         :param size: target size
360
361         """
362         packet_len = len(packet) + 4
363         extend = size - packet_len
364         if extend > 0:
365             packet[Raw].load += ' ' * extend
366
367     def add_packet_info_to_list(self, info):
368         """
369         Add packet info to the testcase's packet info list
370
371         :param info: packet info
372
373         """
374         info.index = len(self.packet_infos)
375         self.packet_infos[info.index] = info
376
377     def create_packet_info(self, src_pg_index, dst_pg_index):
378         """
379         Create packet info object containing the source and destination indexes
380         and add it to the testcase's packet info list
381
382         :param src_pg_index: source packet-generator index
383         :param dst_pg_index: destination packet-generator index
384
385         :returns: _PacketInfo object
386
387         """
388         info = _PacketInfo()
389         self.add_packet_info_to_list(info)
390         info.src = src_pg_index
391         info.dst = dst_pg_index
392         return info
393
394     @staticmethod
395     def info_to_payload(info):
396         """
397         Convert _PacketInfo object to packet payload
398
399         :param info: _PacketInfo object
400
401         :returns: string containing serialized data from packet info
402         """
403         return "%d %d %d" % (info.index, info.src, info.dst)
404
405     @staticmethod
406     def payload_to_info(payload):
407         """
408         Convert packet payload to _PacketInfo object
409
410         :param payload: packet payload
411
412         :returns: _PacketInfo object containing de-serialized data from payload
413
414         """
415         numbers = payload.split()
416         info = _PacketInfo()
417         info.index = int(numbers[0])
418         info.src = int(numbers[1])
419         info.dst = int(numbers[2])
420         return info
421
422     def get_next_packet_info(self, info):
423         """
424         Iterate over the packet info list stored in the testcase
425         Start iteration with first element if info is None
426         Continue based on index in info if info is specified
427
428         :param info: info or None
429         :returns: next info in list or None if no more infos
430         """
431         if info is None:
432             next_index = 0
433         else:
434             next_index = info.index + 1
435         if next_index == len(self.packet_infos):
436             return None
437         else:
438             return self.packet_infos[next_index]
439
440     def get_next_packet_info_for_interface(self, src_index, info):
441         """
442         Search the packet info list for the next packet info with same source
443         interface index
444
445         :param src_index: source interface index to search for
446         :param info: packet info - where to start the search
447         :returns: packet info or None
448
449         """
450         while True:
451             info = self.get_next_packet_info(info)
452             if info is None:
453                 return None
454             if info.src == src_index:
455                 return info
456
457     def get_next_packet_info_for_interface2(self, src_index, dst_index, info):
458         """
459         Search the packet info list for the next packet info with same source
460         and destination interface indexes
461
462         :param src_index: source interface index to search for
463         :param dst_index: destination interface index to search for
464         :param info: packet info - where to start the search
465         :returns: packet info or None
466
467         """
468         while True:
469             info = self.get_next_packet_info_for_interface(src_index, info)
470             if info is None:
471                 return None
472             if info.dst == dst_index:
473                 return info
474
475     def assert_equal(self, real_value, expected_value, name_or_class=None):
476         if name_or_class is None:
477             self.assertEqual(real_value, expected_value, msg)
478             return
479         try:
480             msg = "Invalid %s: %d('%s') does not match expected value %d('%s')"
481             msg = msg % (getdoc(name_or_class).strip(),
482                          real_value, str(name_or_class(real_value)),
483                          expected_value, str(name_or_class(expected_value)))
484         except:
485             msg = "Invalid %s: %s does not match expected value %s" % (
486                 name_or_class, real_value, expected_value)
487
488         self.assertEqual(real_value, expected_value, msg)
489
490     def assert_in_range(
491             self,
492             real_value,
493             expected_min,
494             expected_max,
495             name=None):
496         if name is None:
497             msg = None
498         else:
499             msg = "Invalid %s: %s out of range <%s,%s>" % (
500                 name, real_value, expected_min, expected_max)
501         self.assertTrue(expected_min <= real_value <= expected_max, msg)
502
503
504 class VppTestResult(unittest.TestResult):
505     """
506     @property result_string
507      String variable to store the test case result string.
508     @property errors
509      List variable containing 2-tuples of TestCase instances and strings
510      holding formatted tracebacks. Each tuple represents a test which
511      raised an unexpected exception.
512     @property failures
513      List variable containing 2-tuples of TestCase instances and strings
514      holding formatted tracebacks. Each tuple represents a test where
515      a failure was explicitly signalled using the TestCase.assert*()
516      methods.
517     """
518
519     def __init__(self, stream, descriptions, verbosity):
520         """
521         :param stream File descriptor to store where to report test results. Set
522             to the standard error stream by default.
523         :param descriptions Boolean variable to store information if to use test
524             case descriptions.
525         :param verbosity Integer variable to store required verbosity level.
526         """
527         unittest.TestResult.__init__(self, stream, descriptions, verbosity)
528         self.stream = stream
529         self.descriptions = descriptions
530         self.verbosity = verbosity
531         self.result_string = None
532
533     def addSuccess(self, test):
534         """
535         Record a test succeeded result
536
537         :param test:
538
539         """
540         unittest.TestResult.addSuccess(self, test)
541         self.result_string = colorize("OK", GREEN)
542
543     def addSkip(self, test, reason):
544         """
545         Record a test skipped.
546
547         :param test:
548         :param reason:
549
550         """
551         unittest.TestResult.addSkip(self, test, reason)
552         self.result_string = colorize("SKIP", YELLOW)
553
554     def addFailure(self, test, err):
555         """
556         Record a test failed result
557
558         :param test:
559         :param err: error message
560
561         """
562         unittest.TestResult.addFailure(self, test, err)
563         if hasattr(test, 'tempdir'):
564             self.result_string = colorize("FAIL", RED) + \
565                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
566         else:
567             self.result_string = colorize("FAIL", RED) + ' [no temp dir]'
568
569     def addError(self, test, err):
570         """
571         Record a test error result
572
573         :param test:
574         :param err: error message
575
576         """
577         unittest.TestResult.addError(self, test, err)
578         if hasattr(test, 'tempdir'):
579             self.result_string = colorize("ERROR", RED) + \
580                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
581         else:
582             self.result_string = colorize("ERROR", RED) + ' [no temp dir]'
583
584     def getDescription(self, test):
585         """
586         Get test description
587
588         :param test:
589         :returns: test description
590
591         """
592         # TODO: if none print warning not raise exception
593         short_description = test.shortDescription()
594         if self.descriptions and short_description:
595             return short_description
596         else:
597             return str(test)
598
599     def startTest(self, test):
600         """
601         Start a test
602
603         :param test:
604
605         """
606         unittest.TestResult.startTest(self, test)
607         if self.verbosity > 0:
608             self.stream.writeln(
609                 "Starting " + self.getDescription(test) + " ...")
610             self.stream.writeln(single_line_delim)
611
612     def stopTest(self, test):
613         """
614         Stop a test
615
616         :param test:
617
618         """
619         unittest.TestResult.stopTest(self, test)
620         if self.verbosity > 0:
621             self.stream.writeln(single_line_delim)
622             self.stream.writeln("%-60s%s" %
623                                 (self.getDescription(test), self.result_string))
624             self.stream.writeln(single_line_delim)
625         else:
626             self.stream.writeln("%-60s%s" %
627                                 (self.getDescription(test), self.result_string))
628
629     def printErrors(self):
630         """
631         Print errors from running the test case
632         """
633         self.stream.writeln()
634         self.printErrorList('ERROR', self.errors)
635         self.printErrorList('FAIL', self.failures)
636
637     def printErrorList(self, flavour, errors):
638         """
639         Print error list to the output stream together with error type
640         and test case description.
641
642         :param flavour: error type
643         :param errors: iterable errors
644
645         """
646         for test, err in errors:
647             self.stream.writeln(double_line_delim)
648             self.stream.writeln("%s: %s" %
649                                 (flavour, self.getDescription(test)))
650             self.stream.writeln(single_line_delim)
651             self.stream.writeln("%s" % err)
652
653
654 class VppTestRunner(unittest.TextTestRunner):
655     """
656     A basic test runner implementation which prints results on standard error.
657     """
658     @property
659     def resultclass(self):
660         """Class maintaining the results of the tests"""
661         return VppTestResult
662
663     def run(self, test):
664         """
665         Run the tests
666
667         :param test:
668
669         """
670         print("Running tests using custom test runner")  # debug message
671         return super(VppTestRunner, self).run(test)