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