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