ed71908ca2dfab67df9558f49d45373461b96280
[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         debug_cli = ""
117         if cls.step or cls.debug_gdb or cls.debug_gdbserver:
118             debug_cli = "cli-listen localhost:5002"
119         cls.vpp_cmdline = [cls.vpp_bin, "unix", "{", "nodaemon", debug_cli, "}",
120                            "api-segment", "{", "prefix", cls.shm_prefix, "}"]
121         if cls.plugin_path is not None:
122             cls.vpp_cmdline.extend(["plugin_path", cls.plugin_path])
123         cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline)
124
125     @classmethod
126     def wait_for_enter(cls):
127         if cls.debug_gdbserver:
128             print(double_line_delim)
129             print("Spawned GDB server with PID: %d" % cls.vpp.pid)
130         elif cls.debug_gdb:
131             print(double_line_delim)
132             print("Spawned VPP with PID: %d" % cls.vpp.pid)
133         else:
134             cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid)
135             return
136         print(single_line_delim)
137         print("You can debug the VPP using e.g.:")
138         if cls.debug_gdbserver:
139             print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'")
140             print("Now is the time to attach a gdb by running the above "
141                   "command, set up breakpoints etc. and then resume VPP from "
142                   "within gdb by issuing the 'continue' command")
143         elif cls.debug_gdb:
144             print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid)
145             print("Now is the time to attach a gdb by running the above "
146                   "command and set up breakpoints etc.")
147         print(single_line_delim)
148         raw_input("Press ENTER to continue running the testcase...")
149
150     @classmethod
151     def run_vpp(cls):
152         cmdline = cls.vpp_cmdline
153
154         if cls.debug_gdbserver:
155             cmdline = ['gdbserver', 'localhost:7777'] + cls.vpp_cmdline
156             cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline))
157
158         cls.vpp = subprocess.Popen(cmdline,
159                                    stdout=subprocess.PIPE,
160                                    stderr=subprocess.PIPE,
161                                    bufsize=1)
162         cls.wait_for_enter()
163
164     @classmethod
165     def setUpClass(cls):
166         """
167         Perform class setup before running the testcase
168         Remove shared memory files, start vpp and connect the vpp-api
169         """
170         cls.logger = getLogger(cls.__name__)
171         cls.tempdir = tempfile.mkdtemp(
172             prefix='vpp-unittest-' + cls.__name__ + '-')
173         cls.shm_prefix = cls.tempdir.split("/")[-1]
174         os.chdir(cls.tempdir)
175         cls.logger.info("Temporary dir is %s, shm prefix is %s",
176                         cls.tempdir, cls.shm_prefix)
177         cls.setUpConstants()
178         cls.pg_streams = []
179         cls.packet_infos = {}
180         cls.verbose = 0
181         print(double_line_delim)
182         print(colorize(getdoc(cls), 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_dead = False
189             cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix)
190             if cls.step:
191                 cls.vapi.register_hook(StepHook(cls))
192             else:
193                 cls.vapi.register_hook(PollHook(cls))
194             time.sleep(0.1)
195             try:
196                 cls.vapi.connect()
197             except:
198                 if cls.debug_gdbserver:
199                     print(colorize("You're running VPP inside gdbserver but "
200                                    "VPP-API connection failed, did you forget "
201                                    "to 'continue' VPP from within gdb?", RED))
202                 raise
203             cls.vpp_stdout_queue = Queue()
204             cls.vpp_stdout_reader_thread = Thread(
205                 target=pump_output, args=(cls.vpp.stdout, cls.vpp_stdout_queue))
206             cls.vpp_stdout_reader_thread.start()
207             cls.vpp_stderr_queue = Queue()
208             cls.vpp_stderr_reader_thread = Thread(
209                 target=pump_output, args=(cls.vpp.stderr, cls.vpp_stderr_queue))
210             cls.vpp_stderr_reader_thread.start()
211         except:
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.cli("show int"))
269             self.logger.info(self.vapi.cli("show trace"))
270             self.logger.info(self.vapi.cli("show hardware"))
271             self.logger.info(self.vapi.cli("show error"))
272             self.logger.info(self.vapi.cli("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     @staticmethod
322     def extend_packet(packet, size):
323         """
324         Extend packet to given size by padding with spaces
325         NOTE: Currently works only when Raw layer is present.
326
327         :param packet: packet
328         :param size: target size
329
330         """
331         packet_len = len(packet) + 4
332         extend = size - packet_len
333         if extend > 0:
334             packet[Raw].load += ' ' * extend
335
336     def add_packet_info_to_list(self, info):
337         """
338         Add packet info to the testcase's packet info list
339
340         :param info: packet info
341
342         """
343         info.index = len(self.packet_infos)
344         self.packet_infos[info.index] = info
345
346     def create_packet_info(self, src_pg_index, dst_pg_index):
347         """
348         Create packet info object containing the source and destination indexes
349         and add it to the testcase's packet info list
350
351         :param src_pg_index: source packet-generator index
352         :param dst_pg_index: destination packet-generator index
353
354         :returns: _PacketInfo object
355
356         """
357         info = _PacketInfo()
358         self.add_packet_info_to_list(info)
359         info.src = src_pg_index
360         info.dst = dst_pg_index
361         return info
362
363     @staticmethod
364     def info_to_payload(info):
365         """
366         Convert _PacketInfo object to packet payload
367
368         :param info: _PacketInfo object
369
370         :returns: string containing serialized data from packet info
371         """
372         return "%d %d %d" % (info.index, info.src, info.dst)
373
374     @staticmethod
375     def payload_to_info(payload):
376         """
377         Convert packet payload to _PacketInfo object
378
379         :param payload: packet payload
380
381         :returns: _PacketInfo object containing de-serialized data from payload
382
383         """
384         numbers = payload.split()
385         info = _PacketInfo()
386         info.index = int(numbers[0])
387         info.src = int(numbers[1])
388         info.dst = int(numbers[2])
389         return info
390
391     def get_next_packet_info(self, info):
392         """
393         Iterate over the packet info list stored in the testcase
394         Start iteration with first element if info is None
395         Continue based on index in info if info is specified
396
397         :param info: info or None
398         :returns: next info in list or None if no more infos
399         """
400         if info is None:
401             next_index = 0
402         else:
403             next_index = info.index + 1
404         if next_index == len(self.packet_infos):
405             return None
406         else:
407             return self.packet_infos[next_index]
408
409     def get_next_packet_info_for_interface(self, src_index, info):
410         """
411         Search the packet info list for the next packet info with same source
412         interface index
413
414         :param src_index: source interface index to search for
415         :param info: packet info - where to start the search
416         :returns: packet info or None
417
418         """
419         while True:
420             info = self.get_next_packet_info(info)
421             if info is None:
422                 return None
423             if info.src == src_index:
424                 return info
425
426     def get_next_packet_info_for_interface2(self, src_index, dst_index, info):
427         """
428         Search the packet info list for the next packet info with same source
429         and destination interface indexes
430
431         :param src_index: source interface index to search for
432         :param dst_index: destination interface index to search for
433         :param info: packet info - where to start the search
434         :returns: packet info or None
435
436         """
437         while True:
438             info = self.get_next_packet_info_for_interface(src_index, info)
439             if info is None:
440                 return None
441             if info.dst == dst_index:
442                 return info
443
444
445 class VppTestResult(unittest.TestResult):
446     """
447     @property result_string
448      String variable to store the test case result string.
449     @property errors
450      List variable containing 2-tuples of TestCase instances and strings
451      holding formatted tracebacks. Each tuple represents a test which
452      raised an unexpected exception.
453     @property failures
454      List variable containing 2-tuples of TestCase instances and strings
455      holding formatted tracebacks. Each tuple represents a test where
456      a failure was explicitly signalled using the TestCase.assert*()
457      methods.
458     """
459
460     def __init__(self, stream, descriptions, verbosity):
461         """
462         :param stream File descriptor to store where to report test results. Set
463             to the standard error stream by default.
464         :param descriptions Boolean variable to store information if to use test
465             case descriptions.
466         :param verbosity Integer variable to store required verbosity level.
467         """
468         unittest.TestResult.__init__(self, stream, descriptions, verbosity)
469         self.stream = stream
470         self.descriptions = descriptions
471         self.verbosity = verbosity
472         self.result_string = None
473
474     def addSuccess(self, test):
475         """
476         Record a test succeeded result
477
478         :param test:
479
480         """
481         unittest.TestResult.addSuccess(self, test)
482         self.result_string = colorize("OK", GREEN)
483
484     def addSkip(self, test, reason):
485         """
486         Record a test skipped.
487
488         :param test:
489         :param reason:
490
491         """
492         unittest.TestResult.addSkip(self, test, reason)
493         self.result_string = colorize("SKIP", YELLOW)
494
495     def addFailure(self, test, err):
496         """
497         Record a test failed result
498
499         :param test:
500         :param err: error message
501
502         """
503         unittest.TestResult.addFailure(self, test, err)
504         if hasattr(test, 'tempdir'):
505             self.result_string = colorize("FAIL", RED) + \
506                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
507         else:
508             self.result_string = colorize("FAIL", RED) + ' [no temp dir]'
509
510     def addError(self, test, err):
511         """
512         Record a test error result
513
514         :param test:
515         :param err: error message
516
517         """
518         unittest.TestResult.addError(self, test, err)
519         if hasattr(test, 'tempdir'):
520             self.result_string = colorize("ERROR", RED) + \
521                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
522         else:
523             self.result_string = colorize("ERROR", RED) + ' [no temp dir]'
524
525     def getDescription(self, test):
526         """
527         Get test description
528
529         :param test:
530         :returns: test description
531
532         """
533         # TODO: if none print warning not raise exception
534         short_description = test.shortDescription()
535         if self.descriptions and short_description:
536             return short_description
537         else:
538             return str(test)
539
540     def startTest(self, test):
541         """
542         Start a test
543
544         :param test:
545
546         """
547         unittest.TestResult.startTest(self, test)
548         if self.verbosity > 0:
549             self.stream.writeln(
550                 "Starting " + self.getDescription(test) + " ...")
551             self.stream.writeln(single_line_delim)
552
553     def stopTest(self, test):
554         """
555         Stop a test
556
557         :param test:
558
559         """
560         unittest.TestResult.stopTest(self, test)
561         if self.verbosity > 0:
562             self.stream.writeln(single_line_delim)
563             self.stream.writeln("%-60s%s" %
564                                 (self.getDescription(test), self.result_string))
565             self.stream.writeln(single_line_delim)
566         else:
567             self.stream.writeln("%-60s%s" %
568                                 (self.getDescription(test), self.result_string))
569
570     def printErrors(self):
571         """
572         Print errors from running the test case
573         """
574         self.stream.writeln()
575         self.printErrorList('ERROR', self.errors)
576         self.printErrorList('FAIL', self.failures)
577
578     def printErrorList(self, flavour, errors):
579         """
580         Print error list to the output stream together with error type
581         and test case description.
582
583         :param flavour: error type
584         :param errors: iterable errors
585
586         """
587         for test, err in errors:
588             self.stream.writeln(double_line_delim)
589             self.stream.writeln("%s: %s" %
590                                 (flavour, self.getDescription(test)))
591             self.stream.writeln(single_line_delim)
592             self.stream.writeln("%s" % err)
593
594
595 class VppTestRunner(unittest.TextTestRunner):
596     """
597     A basic test runner implementation which prints results on standard error.
598     """
599     @property
600     def resultclass(self):
601         """Class maintaining the results of the tests"""
602         return VppTestResult
603
604     def run(self, test):
605         """
606         Run the tests
607
608         :param test:
609
610         """
611         print("Running tests using custom test runner")  # debug message
612         return super(VppTestRunner, self).run(test)