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