Update test documentation.
[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         print(double_line_delim)
181         print(colorize(getdoc(cls).splitlines()[0], YELLOW))
182         print(double_line_delim)
183         # need to catch exceptions here because if we raise, then the cleanup
184         # doesn't get called and we might end with a zombie vpp
185         try:
186             cls.run_vpp()
187             cls.vpp_dead = False
188             cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix)
189             if cls.step:
190                 cls.vapi.register_hook(StepHook(cls))
191             else:
192                 cls.vapi.register_hook(PollHook(cls))
193             time.sleep(0.1)
194             try:
195                 cls.vapi.connect()
196             except:
197                 if cls.debug_gdbserver:
198                     print(colorize("You're running VPP inside gdbserver but "
199                                    "VPP-API connection failed, did you forget "
200                                    "to 'continue' VPP from within gdb?", RED))
201                 raise
202             cls.vpp_stdout_queue = Queue()
203             cls.vpp_stdout_reader_thread = Thread(
204                 target=pump_output, args=(cls.vpp.stdout, cls.vpp_stdout_queue))
205             cls.vpp_stdout_reader_thread.start()
206             cls.vpp_stderr_queue = Queue()
207             cls.vpp_stderr_reader_thread = Thread(
208                 target=pump_output, args=(cls.vpp.stderr, cls.vpp_stderr_queue))
209             cls.vpp_stderr_reader_thread.start()
210         except:
211             if hasattr(cls, 'vpp'):
212                 cls.vpp.terminate()
213                 del cls.vpp
214             raise
215
216     @classmethod
217     def quit(cls):
218         """
219         Disconnect vpp-api, kill vpp and cleanup shared memory files
220         """
221         if (cls.debug_gdbserver or cls.debug_gdb) and hasattr(cls, 'vpp'):
222             cls.vpp.poll()
223             if cls.vpp.returncode is None:
224                 print(double_line_delim)
225                 print("VPP or GDB server is still running")
226                 print(single_line_delim)
227                 raw_input("When done debugging, press ENTER to kill the process"
228                           " and finish running the testcase...")
229
230         if hasattr(cls, 'vpp'):
231             cls.vapi.disconnect()
232             cls.vpp.poll()
233             if cls.vpp.returncode is None:
234                 cls.vpp.terminate()
235             del cls.vpp
236
237         if hasattr(cls, 'vpp_stdout_queue'):
238             cls.logger.info(single_line_delim)
239             cls.logger.info('VPP output to stdout while running %s:',
240                             cls.__name__)
241             cls.logger.info(single_line_delim)
242             f = open(cls.tempdir + '/vpp_stdout.txt', 'w')
243             while not cls.vpp_stdout_queue.empty():
244                 line = cls.vpp_stdout_queue.get_nowait()
245                 f.write(line)
246                 cls.logger.info('VPP stdout: %s' % line.rstrip('\n'))
247
248         if hasattr(cls, 'vpp_stderr_queue'):
249             cls.logger.info(single_line_delim)
250             cls.logger.info('VPP output to stderr while running %s:',
251                             cls.__name__)
252             cls.logger.info(single_line_delim)
253             f = open(cls.tempdir + '/vpp_stderr.txt', 'w')
254             while not cls.vpp_stderr_queue.empty():
255                 line = cls.vpp_stderr_queue.get_nowait()
256                 f.write(line)
257                 cls.logger.info('VPP stderr: %s' % line.rstrip('\n'))
258             cls.logger.info(single_line_delim)
259
260     @classmethod
261     def tearDownClass(cls):
262         """ Perform final cleanup after running all tests in this test-case """
263         cls.quit()
264
265     def tearDown(self):
266         """ Show various debug prints after each test """
267         if not self.vpp_dead:
268             self.logger.info(self.vapi.ppcli("show int"))
269             self.logger.debug(self.vapi.cli("show trace"))
270             self.logger.info(self.vapi.ppcli("show hardware"))
271             self.logger.info(self.vapi.ppcli("show error"))
272             self.logger.info(self.vapi.ppcli("show run"))
273
274     def setUp(self):
275         """ Clear trace before running each test"""
276         self.vapi.cli("clear trace")
277         # store the test instance inside the test class - so that objects
278         # holding the class can access instance methods (like assertEqual)
279         type(self).test_instance = self
280
281     @classmethod
282     def pg_enable_capture(cls, interfaces):
283         """
284         Enable capture on packet-generator interfaces
285
286         :param interfaces: iterable interface indexes
287
288         """
289         for i in interfaces:
290             i.enable_capture()
291
292     @classmethod
293     def pg_start(cls):
294         """
295         Enable the packet-generator and send all prepared packet streams
296         Remove the packet streams afterwards
297         """
298         cls.vapi.cli("trace add pg-input 50")  # 50 is maximum
299         cls.vapi.cli('packet-generator enable')
300         sleep(1)  # give VPP some time to process the packets
301         for stream in cls.pg_streams:
302             cls.vapi.cli('packet-generator delete %s' % stream)
303         cls.pg_streams = []
304
305     @classmethod
306     def create_pg_interfaces(cls, interfaces):
307         """
308         Create packet-generator interfaces
309
310         :param interfaces: iterable indexes of the interfaces
311
312         """
313         result = []
314         for i in interfaces:
315             intf = VppPGInterface(cls, i)
316             setattr(cls, intf.name, intf)
317             result.append(intf)
318         cls.pg_interfaces = result
319         return result
320
321     @classmethod
322     def create_loopback_interfaces(cls, interfaces):
323         """
324         Create loopback interfaces
325
326         :param interfaces: iterable indexes of the interfaces
327
328         """
329         result = []
330         for i in interfaces:
331             intf = VppLoInterface(cls, i)
332             setattr(cls, intf.name, intf)
333             result.append(intf)
334         cls.lo_interfaces = result
335         return result
336
337     @staticmethod
338     def extend_packet(packet, size):
339         """
340         Extend packet to given size by padding with spaces
341         NOTE: Currently works only when Raw layer is present.
342
343         :param packet: packet
344         :param size: target size
345
346         """
347         packet_len = len(packet) + 4
348         extend = size - packet_len
349         if extend > 0:
350             packet[Raw].load += ' ' * extend
351
352     def add_packet_info_to_list(self, info):
353         """
354         Add packet info to the testcase's packet info list
355
356         :param info: packet info
357
358         """
359         info.index = len(self.packet_infos)
360         self.packet_infos[info.index] = info
361
362     def create_packet_info(self, src_pg_index, dst_pg_index):
363         """
364         Create packet info object containing the source and destination indexes
365         and add it to the testcase's packet info list
366
367         :param src_pg_index: source packet-generator index
368         :param dst_pg_index: destination packet-generator index
369
370         :returns: _PacketInfo object
371
372         """
373         info = _PacketInfo()
374         self.add_packet_info_to_list(info)
375         info.src = src_pg_index
376         info.dst = dst_pg_index
377         return info
378
379     @staticmethod
380     def info_to_payload(info):
381         """
382         Convert _PacketInfo object to packet payload
383
384         :param info: _PacketInfo object
385
386         :returns: string containing serialized data from packet info
387         """
388         return "%d %d %d" % (info.index, info.src, info.dst)
389
390     @staticmethod
391     def payload_to_info(payload):
392         """
393         Convert packet payload to _PacketInfo object
394
395         :param payload: packet payload
396
397         :returns: _PacketInfo object containing de-serialized data from payload
398
399         """
400         numbers = payload.split()
401         info = _PacketInfo()
402         info.index = int(numbers[0])
403         info.src = int(numbers[1])
404         info.dst = int(numbers[2])
405         return info
406
407     def get_next_packet_info(self, info):
408         """
409         Iterate over the packet info list stored in the testcase
410         Start iteration with first element if info is None
411         Continue based on index in info if info is specified
412
413         :param info: info or None
414         :returns: next info in list or None if no more infos
415         """
416         if info is None:
417             next_index = 0
418         else:
419             next_index = info.index + 1
420         if next_index == len(self.packet_infos):
421             return None
422         else:
423             return self.packet_infos[next_index]
424
425     def get_next_packet_info_for_interface(self, src_index, info):
426         """
427         Search the packet info list for the next packet info with same source
428         interface index
429
430         :param src_index: source interface index to search for
431         :param info: packet info - where to start the search
432         :returns: packet info or None
433
434         """
435         while True:
436             info = self.get_next_packet_info(info)
437             if info is None:
438                 return None
439             if info.src == src_index:
440                 return info
441
442     def get_next_packet_info_for_interface2(self, src_index, dst_index, info):
443         """
444         Search the packet info list for the next packet info with same source
445         and destination interface indexes
446
447         :param src_index: source interface index to search for
448         :param dst_index: destination interface index to search for
449         :param info: packet info - where to start the search
450         :returns: packet info or None
451
452         """
453         while True:
454             info = self.get_next_packet_info_for_interface(src_index, info)
455             if info is None:
456                 return None
457             if info.dst == dst_index:
458                 return info
459
460
461 class VppTestResult(unittest.TestResult):
462     """
463     @property result_string
464      String variable to store the test case result string.
465     @property errors
466      List variable containing 2-tuples of TestCase instances and strings
467      holding formatted tracebacks. Each tuple represents a test which
468      raised an unexpected exception.
469     @property failures
470      List variable containing 2-tuples of TestCase instances and strings
471      holding formatted tracebacks. Each tuple represents a test where
472      a failure was explicitly signalled using the TestCase.assert*()
473      methods.
474     """
475
476     def __init__(self, stream, descriptions, verbosity):
477         """
478         :param stream File descriptor to store where to report test results. Set
479             to the standard error stream by default.
480         :param descriptions Boolean variable to store information if to use test
481             case descriptions.
482         :param verbosity Integer variable to store required verbosity level.
483         """
484         unittest.TestResult.__init__(self, stream, descriptions, verbosity)
485         self.stream = stream
486         self.descriptions = descriptions
487         self.verbosity = verbosity
488         self.result_string = None
489
490     def addSuccess(self, test):
491         """
492         Record a test succeeded result
493
494         :param test:
495
496         """
497         unittest.TestResult.addSuccess(self, test)
498         self.result_string = colorize("OK", GREEN)
499
500     def addSkip(self, test, reason):
501         """
502         Record a test skipped.
503
504         :param test:
505         :param reason:
506
507         """
508         unittest.TestResult.addSkip(self, test, reason)
509         self.result_string = colorize("SKIP", YELLOW)
510
511     def addFailure(self, test, err):
512         """
513         Record a test failed result
514
515         :param test:
516         :param err: error message
517
518         """
519         unittest.TestResult.addFailure(self, test, err)
520         if hasattr(test, 'tempdir'):
521             self.result_string = colorize("FAIL", RED) + \
522                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
523         else:
524             self.result_string = colorize("FAIL", RED) + ' [no temp dir]'
525
526     def addError(self, test, err):
527         """
528         Record a test error result
529
530         :param test:
531         :param err: error message
532
533         """
534         unittest.TestResult.addError(self, test, err)
535         if hasattr(test, 'tempdir'):
536             self.result_string = colorize("ERROR", RED) + \
537                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
538         else:
539             self.result_string = colorize("ERROR", RED) + ' [no temp dir]'
540
541     def getDescription(self, test):
542         """
543         Get test description
544
545         :param test:
546         :returns: test description
547
548         """
549         # TODO: if none print warning not raise exception
550         short_description = test.shortDescription()
551         if self.descriptions and short_description:
552             return short_description
553         else:
554             return str(test)
555
556     def startTest(self, test):
557         """
558         Start a test
559
560         :param test:
561
562         """
563         unittest.TestResult.startTest(self, test)
564         if self.verbosity > 0:
565             self.stream.writeln(
566                 "Starting " + self.getDescription(test) + " ...")
567             self.stream.writeln(single_line_delim)
568
569     def stopTest(self, test):
570         """
571         Stop a test
572
573         :param test:
574
575         """
576         unittest.TestResult.stopTest(self, test)
577         if self.verbosity > 0:
578             self.stream.writeln(single_line_delim)
579             self.stream.writeln("%-60s%s" %
580                                 (self.getDescription(test), self.result_string))
581             self.stream.writeln(single_line_delim)
582         else:
583             self.stream.writeln("%-60s%s" %
584                                 (self.getDescription(test), self.result_string))
585
586     def printErrors(self):
587         """
588         Print errors from running the test case
589         """
590         self.stream.writeln()
591         self.printErrorList('ERROR', self.errors)
592         self.printErrorList('FAIL', self.failures)
593
594     def printErrorList(self, flavour, errors):
595         """
596         Print error list to the output stream together with error type
597         and test case description.
598
599         :param flavour: error type
600         :param errors: iterable errors
601
602         """
603         for test, err in errors:
604             self.stream.writeln(double_line_delim)
605             self.stream.writeln("%s: %s" %
606                                 (flavour, self.getDescription(test)))
607             self.stream.writeln(single_line_delim)
608             self.stream.writeln("%s" % err)
609
610
611 class VppTestRunner(unittest.TextTestRunner):
612     """
613     A basic test runner implementation which prints results on standard error.
614     """
615     @property
616     def resultclass(self):
617         """Class maintaining the results of the tests"""
618         return VppTestResult
619
620     def run(self, test):
621         """
622         Run the tests
623
624         :param test:
625
626         """
627         print("Running tests using custom test runner")  # debug message
628         return super(VppTestRunner, self).run(test)