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