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