make test: don't rely on cPython GC to close fds
[vpp.git] / test / framework.py
1 #!/usr/bin/env python
2
3 from __future__ import print_function
4 import gc
5 import sys
6 import os
7 import select
8 import unittest
9 import tempfile
10 import time
11 import resource
12 from collections import deque
13 from threading import Thread, Event
14 from inspect import getdoc
15 from traceback import format_exception
16 from logging import FileHandler, DEBUG, Formatter
17 from scapy.packet import Raw
18 from hook import StepHook, PollHook
19 from vpp_pg_interface import VppPGInterface
20 from vpp_sub_interface import VppSubInterface
21 from vpp_lo_interface import VppLoInterface
22 from vpp_papi_provider import VppPapiProvider
23 from log import *
24 from vpp_object import VppObjectRegistry
25 if os.name == 'posix' and sys.version_info[0] < 3:
26     # using subprocess32 is recommended by python official documentation
27     # @ https://docs.python.org/2/library/subprocess.html
28     import subprocess32 as subprocess
29 else:
30     import subprocess
31
32 """
33   Test framework module.
34
35   The module provides a set of tools for constructing and running tests and
36   representing the results.
37 """
38
39
40 class _PacketInfo(object):
41     """Private class to create packet info object.
42
43     Help process information about the next packet.
44     Set variables to default values.
45     """
46     #: Store the index of the packet.
47     index = -1
48     #: Store the index of the source packet generator interface of the packet.
49     src = -1
50     #: Store the index of the destination packet generator interface
51     #: of the packet.
52     dst = -1
53     #: Store expected ip version
54     ip = -1
55     #: Store expected upper protocol
56     proto = -1
57     #: Store the copy of the former packet.
58     data = None
59
60     def __eq__(self, other):
61         index = self.index == other.index
62         src = self.src == other.src
63         dst = self.dst == other.dst
64         data = self.data == other.data
65         return index and src and dst and data
66
67
68 def pump_output(testclass):
69     """ pump output from vpp stdout/stderr to proper queues """
70     while not testclass.pump_thread_stop_flag.wait(0):
71         readable = select.select([testclass.vpp.stdout.fileno(),
72                                   testclass.vpp.stderr.fileno(),
73                                   testclass.pump_thread_wakeup_pipe[0]],
74                                  [], [])[0]
75         if testclass.vpp.stdout.fileno() in readable:
76             read = os.read(testclass.vpp.stdout.fileno(), 1024)
77             testclass.vpp_stdout_deque.append(read)
78         if testclass.vpp.stderr.fileno() in readable:
79             read = os.read(testclass.vpp.stderr.fileno(), 1024)
80             testclass.vpp_stderr_deque.append(read)
81         # ignoring the dummy pipe here intentionally - the flag will take care
82         # of properly terminating the loop
83
84
85 def running_extended_tests():
86     try:
87         s = os.getenv("EXTENDED_TESTS")
88         return True if s.lower() in ("y", "yes", "1") else False
89     except:
90         return False
91     return False
92
93
94 class VppTestCase(unittest.TestCase):
95     """This subclass is a base class for VPP test cases that are implemented as
96     classes. It provides methods to create and run test case.
97     """
98
99     @property
100     def packet_infos(self):
101         """List of packet infos"""
102         return self._packet_infos
103
104     @classmethod
105     def get_packet_count_for_if_idx(cls, dst_if_index):
106         """Get the number of packet info for specified destination if index"""
107         if dst_if_index in cls._packet_count_for_dst_if_idx:
108             return cls._packet_count_for_dst_if_idx[dst_if_index]
109         else:
110             return 0
111
112     @classmethod
113     def instance(cls):
114         """Return the instance of this testcase"""
115         return cls.test_instance
116
117     @classmethod
118     def set_debug_flags(cls, d):
119         cls.debug_core = False
120         cls.debug_gdb = False
121         cls.debug_gdbserver = False
122         if d is None:
123             return
124         dl = d.lower()
125         if dl == "core":
126             cls.debug_core = True
127         elif dl == "gdb":
128             cls.debug_gdb = True
129         elif dl == "gdbserver":
130             cls.debug_gdbserver = True
131         else:
132             raise Exception("Unrecognized DEBUG option: '%s'" % d)
133
134     @classmethod
135     def setUpConstants(cls):
136         """ Set-up the test case class based on environment variables """
137         try:
138             s = os.getenv("STEP")
139             cls.step = True if s.lower() in ("y", "yes", "1") else False
140         except:
141             cls.step = False
142         try:
143             d = os.getenv("DEBUG")
144         except:
145             d = None
146         cls.set_debug_flags(d)
147         cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp")
148         cls.plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH')
149         cls.extern_plugin_path = os.getenv('EXTERN_PLUGINS')
150         plugin_path = None
151         if cls.plugin_path is not None:
152             if cls.extern_plugin_path is not None:
153                 plugin_path = "%s:%s" % (
154                     cls.plugin_path, cls.extern_plugin_path)
155             else:
156                 plugin_path = cls.plugin_path
157         elif cls.extern_plugin_path is not None:
158             plugin_path = cls.extern_plugin_path
159         debug_cli = ""
160         if cls.step or cls.debug_gdb or cls.debug_gdbserver:
161             debug_cli = "cli-listen localhost:5002"
162         coredump_size = None
163         try:
164             size = os.getenv("COREDUMP_SIZE")
165             if size is not None:
166                 coredump_size = "coredump-size %s" % size
167         except:
168             pass
169         if coredump_size is None:
170             coredump_size = "coredump-size unlimited"
171         cls.vpp_cmdline = [cls.vpp_bin, "unix",
172                            "{", "nodaemon", debug_cli, coredump_size, "}",
173                            "api-trace", "{", "on", "}",
174                            "api-segment", "{", "prefix", cls.shm_prefix, "}",
175                            "plugins", "{", "plugin", "dpdk_plugin.so", "{",
176                            "disable", "}", "}"]
177         if plugin_path is not None:
178             cls.vpp_cmdline.extend(["plugin_path", plugin_path])
179         cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline)
180
181     @classmethod
182     def wait_for_enter(cls):
183         if cls.debug_gdbserver:
184             print(double_line_delim)
185             print("Spawned GDB server with PID: %d" % cls.vpp.pid)
186         elif cls.debug_gdb:
187             print(double_line_delim)
188             print("Spawned VPP with PID: %d" % cls.vpp.pid)
189         else:
190             cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid)
191             return
192         print(single_line_delim)
193         print("You can debug the VPP using e.g.:")
194         if cls.debug_gdbserver:
195             print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'")
196             print("Now is the time to attach a gdb by running the above "
197                   "command, set up breakpoints etc. and then resume VPP from "
198                   "within gdb by issuing the 'continue' command")
199         elif cls.debug_gdb:
200             print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid)
201             print("Now is the time to attach a gdb by running the above "
202                   "command and set up breakpoints etc.")
203         print(single_line_delim)
204         raw_input("Press ENTER to continue running the testcase...")
205
206     @classmethod
207     def run_vpp(cls):
208         cmdline = cls.vpp_cmdline
209
210         if cls.debug_gdbserver:
211             gdbserver = '/usr/bin/gdbserver'
212             if not os.path.isfile(gdbserver) or \
213                     not os.access(gdbserver, os.X_OK):
214                 raise Exception("gdbserver binary '%s' does not exist or is "
215                                 "not executable" % gdbserver)
216
217             cmdline = [gdbserver, 'localhost:7777'] + cls.vpp_cmdline
218             cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline))
219
220         try:
221             cls.vpp = subprocess.Popen(cmdline,
222                                        stdout=subprocess.PIPE,
223                                        stderr=subprocess.PIPE,
224                                        bufsize=1)
225         except Exception as e:
226             cls.logger.critical("Couldn't start vpp: %s" % e)
227             raise
228
229         cls.wait_for_enter()
230
231     @classmethod
232     def setUpClass(cls):
233         """
234         Perform class setup before running the testcase
235         Remove shared memory files, start vpp and connect the vpp-api
236         """
237         gc.collect()  # run garbage collection first
238         cls.logger = getLogger(cls.__name__)
239         cls.tempdir = tempfile.mkdtemp(
240             prefix='vpp-unittest-' + cls.__name__ + '-')
241         cls.file_handler = FileHandler("%s/log.txt" % cls.tempdir)
242         cls.file_handler.setFormatter(
243             Formatter(fmt='%(asctime)s,%(msecs)03d %(message)s',
244                       datefmt="%H:%M:%S"))
245         cls.file_handler.setLevel(DEBUG)
246         cls.logger.addHandler(cls.file_handler)
247         cls.shm_prefix = cls.tempdir.split("/")[-1]
248         os.chdir(cls.tempdir)
249         cls.logger.info("Temporary dir is %s, shm prefix is %s",
250                         cls.tempdir, cls.shm_prefix)
251         cls.setUpConstants()
252         cls.reset_packet_infos()
253         cls._captures = []
254         cls._zombie_captures = []
255         cls.verbose = 0
256         cls.vpp_dead = False
257         cls.registry = VppObjectRegistry()
258         cls.vpp_startup_failed = False
259         # need to catch exceptions here because if we raise, then the cleanup
260         # doesn't get called and we might end with a zombie vpp
261         try:
262             cls.run_vpp()
263             cls.vpp_stdout_deque = deque()
264             cls.vpp_stderr_deque = deque()
265             cls.pump_thread_stop_flag = Event()
266             cls.pump_thread_wakeup_pipe = os.pipe()
267             cls.pump_thread = Thread(target=pump_output, args=(cls,))
268             cls.pump_thread.daemon = True
269             cls.pump_thread.start()
270             cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix, cls)
271             if cls.step:
272                 hook = StepHook(cls)
273             else:
274                 hook = PollHook(cls)
275             cls.vapi.register_hook(hook)
276             cls.sleep(0.1, "after vpp startup, before initial poll")
277             try:
278                 hook.poll_vpp()
279             except:
280                 cls.vpp_startup_failed = True
281                 cls.logger.critical(
282                     "VPP died shortly after startup, check the"
283                     " output to standard error for possible cause")
284                 raise
285             try:
286                 cls.vapi.connect()
287             except:
288                 if cls.debug_gdbserver:
289                     print(colorize("You're running VPP inside gdbserver but "
290                                    "VPP-API connection failed, did you forget "
291                                    "to 'continue' VPP from within gdb?", RED))
292                 raise
293         except:
294             t, v, tb = sys.exc_info()
295             try:
296                 cls.quit()
297             except:
298                 pass
299             raise t, v, tb
300
301     @classmethod
302     def quit(cls):
303         """
304         Disconnect vpp-api, kill vpp and cleanup shared memory files
305         """
306         if (cls.debug_gdbserver or cls.debug_gdb) and hasattr(cls, 'vpp'):
307             cls.vpp.poll()
308             if cls.vpp.returncode is None:
309                 print(double_line_delim)
310                 print("VPP or GDB server is still running")
311                 print(single_line_delim)
312                 raw_input("When done debugging, press ENTER to kill the "
313                           "process and finish running the testcase...")
314
315         os.write(cls.pump_thread_wakeup_pipe[1], 'ding dong wake up')
316         cls.pump_thread_stop_flag.set()
317         if hasattr(cls, 'pump_thread'):
318             cls.logger.debug("Waiting for pump thread to stop")
319             cls.pump_thread.join()
320         if hasattr(cls, 'vpp_stderr_reader_thread'):
321             cls.logger.debug("Waiting for stdderr pump to stop")
322             cls.vpp_stderr_reader_thread.join()
323
324         if hasattr(cls, 'vpp'):
325             if hasattr(cls, 'vapi'):
326                 cls.vapi.disconnect()
327                 del cls.vapi
328             cls.vpp.poll()
329             if cls.vpp.returncode is None:
330                 cls.logger.debug("Sending TERM to vpp")
331                 cls.vpp.terminate()
332                 cls.logger.debug("Waiting for vpp to die")
333                 cls.vpp.communicate()
334             del cls.vpp
335
336         if cls.vpp_startup_failed:
337             stdout_log = cls.logger.info
338             stderr_log = cls.logger.critical
339         else:
340             stdout_log = cls.logger.info
341             stderr_log = cls.logger.info
342
343         if hasattr(cls, 'vpp_stdout_deque'):
344             stdout_log(single_line_delim)
345             stdout_log('VPP output to stdout while running %s:', cls.__name__)
346             stdout_log(single_line_delim)
347             vpp_output = "".join(cls.vpp_stdout_deque)
348             with open(cls.tempdir + '/vpp_stdout.txt', 'w') as f:
349                 f.write(vpp_output)
350             stdout_log('\n%s', vpp_output)
351             stdout_log(single_line_delim)
352
353         if hasattr(cls, 'vpp_stderr_deque'):
354             stderr_log(single_line_delim)
355             stderr_log('VPP output to stderr while running %s:', cls.__name__)
356             stderr_log(single_line_delim)
357             vpp_output = "".join(cls.vpp_stderr_deque)
358             with open(cls.tempdir + '/vpp_stderr.txt', 'w') as f:
359                 f.write(vpp_output)
360             stderr_log('\n%s', vpp_output)
361             stderr_log(single_line_delim)
362
363     @classmethod
364     def tearDownClass(cls):
365         """ Perform final cleanup after running all tests in this test-case """
366         cls.quit()
367         cls.file_handler.close()
368
369     def tearDown(self):
370         """ Show various debug prints after each test """
371         self.logger.debug("--- tearDown() for %s.%s(%s) called ---" %
372                           (self.__class__.__name__, self._testMethodName,
373                            self._testMethodDoc))
374         if not self.vpp_dead:
375             self.logger.debug(self.vapi.cli("show trace"))
376             self.logger.info(self.vapi.ppcli("show interface"))
377             self.logger.info(self.vapi.ppcli("show hardware"))
378             self.logger.info(self.vapi.ppcli("show error"))
379             self.logger.info(self.vapi.ppcli("show run"))
380             self.registry.remove_vpp_config(self.logger)
381             # Save/Dump VPP api trace log
382             api_trace = "vpp_api_trace.%s.log" % self._testMethodName
383             tmp_api_trace = "/tmp/%s" % api_trace
384             vpp_api_trace_log = "%s/%s" % (self.tempdir, api_trace)
385             self.logger.info(self.vapi.ppcli("api trace save %s" % api_trace))
386             self.logger.info("Moving %s to %s\n" % (tmp_api_trace,
387                                                     vpp_api_trace_log))
388             os.rename(tmp_api_trace, vpp_api_trace_log)
389             self.logger.info(self.vapi.ppcli("api trace dump %s" %
390                                              vpp_api_trace_log))
391         else:
392             self.registry.unregister_all(self.logger)
393
394     def setUp(self):
395         """ Clear trace before running each test"""
396         self.logger.debug("--- setUp() for %s.%s(%s) called ---" %
397                           (self.__class__.__name__, self._testMethodName,
398                            self._testMethodDoc))
399         if self.vpp_dead:
400             raise Exception("VPP is dead when setting up the test")
401         self.sleep(.1, "during setUp")
402         self.vpp_stdout_deque.append(
403             "--- test setUp() for %s.%s(%s) starts here ---\n" %
404             (self.__class__.__name__, self._testMethodName,
405              self._testMethodDoc))
406         self.vpp_stderr_deque.append(
407             "--- test setUp() for %s.%s(%s) starts here ---\n" %
408             (self.__class__.__name__, self._testMethodName,
409              self._testMethodDoc))
410         self.vapi.cli("clear trace")
411         # store the test instance inside the test class - so that objects
412         # holding the class can access instance methods (like assertEqual)
413         type(self).test_instance = self
414
415     @classmethod
416     def pg_enable_capture(cls, interfaces):
417         """
418         Enable capture on packet-generator interfaces
419
420         :param interfaces: iterable interface indexes
421
422         """
423         for i in interfaces:
424             i.enable_capture()
425
426     @classmethod
427     def register_capture(cls, cap_name):
428         """ Register a capture in the testclass """
429         # add to the list of captures with current timestamp
430         cls._captures.append((time.time(), cap_name))
431         # filter out from zombies
432         cls._zombie_captures = [(stamp, name)
433                                 for (stamp, name) in cls._zombie_captures
434                                 if name != cap_name]
435
436     @classmethod
437     def pg_start(cls):
438         """ Remove any zombie captures and enable the packet generator """
439         # how long before capture is allowed to be deleted - otherwise vpp
440         # crashes - 100ms seems enough (this shouldn't be needed at all)
441         capture_ttl = 0.1
442         now = time.time()
443         for stamp, cap_name in cls._zombie_captures:
444             wait = stamp + capture_ttl - now
445             if wait > 0:
446                 cls.sleep(wait, "before deleting capture %s" % cap_name)
447                 now = time.time()
448             cls.logger.debug("Removing zombie capture %s" % cap_name)
449             cls.vapi.cli('packet-generator delete %s' % cap_name)
450
451         cls.vapi.cli("trace add pg-input 50")  # 50 is maximum
452         cls.vapi.cli('packet-generator enable')
453         cls._zombie_captures = cls._captures
454         cls._captures = []
455
456     @classmethod
457     def create_pg_interfaces(cls, interfaces):
458         """
459         Create packet-generator interfaces.
460
461         :param interfaces: iterable indexes of the interfaces.
462         :returns: List of created interfaces.
463
464         """
465         result = []
466         for i in interfaces:
467             intf = VppPGInterface(cls, i)
468             setattr(cls, intf.name, intf)
469             result.append(intf)
470         cls.pg_interfaces = result
471         return result
472
473     @classmethod
474     def create_loopback_interfaces(cls, interfaces):
475         """
476         Create loopback interfaces.
477
478         :param interfaces: iterable indexes of the interfaces.
479         :returns: List of created interfaces.
480         """
481         result = []
482         for i in interfaces:
483             intf = VppLoInterface(cls, i)
484             setattr(cls, intf.name, intf)
485             result.append(intf)
486         cls.lo_interfaces = result
487         return result
488
489     @staticmethod
490     def extend_packet(packet, size):
491         """
492         Extend packet to given size by padding with spaces
493         NOTE: Currently works only when Raw layer is present.
494
495         :param packet: packet
496         :param size: target size
497
498         """
499         packet_len = len(packet) + 4
500         extend = size - packet_len
501         if extend > 0:
502             packet[Raw].load += ' ' * extend
503
504     @classmethod
505     def reset_packet_infos(cls):
506         """ Reset the list of packet info objects and packet counts to zero """
507         cls._packet_infos = {}
508         cls._packet_count_for_dst_if_idx = {}
509
510     @classmethod
511     def create_packet_info(cls, src_if, dst_if):
512         """
513         Create packet info object containing the source and destination indexes
514         and add it to the testcase's packet info list
515
516         :param VppInterface src_if: source interface
517         :param VppInterface dst_if: destination interface
518
519         :returns: _PacketInfo object
520
521         """
522         info = _PacketInfo()
523         info.index = len(cls._packet_infos)
524         info.src = src_if.sw_if_index
525         info.dst = dst_if.sw_if_index
526         if isinstance(dst_if, VppSubInterface):
527             dst_idx = dst_if.parent.sw_if_index
528         else:
529             dst_idx = dst_if.sw_if_index
530         if dst_idx in cls._packet_count_for_dst_if_idx:
531             cls._packet_count_for_dst_if_idx[dst_idx] += 1
532         else:
533             cls._packet_count_for_dst_if_idx[dst_idx] = 1
534         cls._packet_infos[info.index] = info
535         return info
536
537     @staticmethod
538     def info_to_payload(info):
539         """
540         Convert _PacketInfo object to packet payload
541
542         :param info: _PacketInfo object
543
544         :returns: string containing serialized data from packet info
545         """
546         return "%d %d %d %d %d" % (info.index, info.src, info.dst,
547                                    info.ip, info.proto)
548
549     @staticmethod
550     def payload_to_info(payload):
551         """
552         Convert packet payload to _PacketInfo object
553
554         :param payload: packet payload
555
556         :returns: _PacketInfo object containing de-serialized data from payload
557
558         """
559         numbers = payload.split()
560         info = _PacketInfo()
561         info.index = int(numbers[0])
562         info.src = int(numbers[1])
563         info.dst = int(numbers[2])
564         info.ip = int(numbers[3])
565         info.proto = int(numbers[4])
566         return info
567
568     def get_next_packet_info(self, info):
569         """
570         Iterate over the packet info list stored in the testcase
571         Start iteration with first element if info is None
572         Continue based on index in info if info is specified
573
574         :param info: info or None
575         :returns: next info in list or None if no more infos
576         """
577         if info is None:
578             next_index = 0
579         else:
580             next_index = info.index + 1
581         if next_index == len(self._packet_infos):
582             return None
583         else:
584             return self._packet_infos[next_index]
585
586     def get_next_packet_info_for_interface(self, src_index, info):
587         """
588         Search the packet info list for the next packet info with same source
589         interface index
590
591         :param src_index: source interface index to search for
592         :param info: packet info - where to start the search
593         :returns: packet info or None
594
595         """
596         while True:
597             info = self.get_next_packet_info(info)
598             if info is None:
599                 return None
600             if info.src == src_index:
601                 return info
602
603     def get_next_packet_info_for_interface2(self, src_index, dst_index, info):
604         """
605         Search the packet info list for the next packet info with same source
606         and destination interface indexes
607
608         :param src_index: source interface index to search for
609         :param dst_index: destination interface index to search for
610         :param info: packet info - where to start the search
611         :returns: packet info or None
612
613         """
614         while True:
615             info = self.get_next_packet_info_for_interface(src_index, info)
616             if info is None:
617                 return None
618             if info.dst == dst_index:
619                 return info
620
621     def assert_equal(self, real_value, expected_value, name_or_class=None):
622         if name_or_class is None:
623             self.assertEqual(real_value, expected_value)
624             return
625         try:
626             msg = "Invalid %s: %d('%s') does not match expected value %d('%s')"
627             msg = msg % (getdoc(name_or_class).strip(),
628                          real_value, str(name_or_class(real_value)),
629                          expected_value, str(name_or_class(expected_value)))
630         except:
631             msg = "Invalid %s: %s does not match expected value %s" % (
632                 name_or_class, real_value, expected_value)
633
634         self.assertEqual(real_value, expected_value, msg)
635
636     def assert_in_range(self,
637                         real_value,
638                         expected_min,
639                         expected_max,
640                         name=None):
641         if name is None:
642             msg = None
643         else:
644             msg = "Invalid %s: %s out of range <%s,%s>" % (
645                 name, real_value, expected_min, expected_max)
646         self.assertTrue(expected_min <= real_value <= expected_max, msg)
647
648     @classmethod
649     def sleep(cls, timeout, remark=None):
650         if hasattr(cls, 'logger'):
651             cls.logger.debug("Sleeping for %ss (%s)" % (timeout, remark))
652         time.sleep(timeout)
653
654
655 class TestCasePrinter(object):
656     _shared_state = {}
657
658     def __init__(self):
659         self.__dict__ = self._shared_state
660         if not hasattr(self, "_test_case_set"):
661             self._test_case_set = set()
662
663     def print_test_case_heading_if_first_time(self, case):
664         if case.__class__ not in self._test_case_set:
665             print(double_line_delim)
666             print(colorize(getdoc(case.__class__).splitlines()[0], YELLOW))
667             print(double_line_delim)
668             self._test_case_set.add(case.__class__)
669
670
671 class VppTestResult(unittest.TestResult):
672     """
673     @property result_string
674      String variable to store the test case result string.
675     @property errors
676      List variable containing 2-tuples of TestCase instances and strings
677      holding formatted tracebacks. Each tuple represents a test which
678      raised an unexpected exception.
679     @property failures
680      List variable containing 2-tuples of TestCase instances and strings
681      holding formatted tracebacks. Each tuple represents a test where
682      a failure was explicitly signalled using the TestCase.assert*()
683      methods.
684     """
685
686     def __init__(self, stream, descriptions, verbosity):
687         """
688         :param stream File descriptor to store where to report test results.
689             Set to the standard error stream by default.
690         :param descriptions Boolean variable to store information if to use
691             test case descriptions.
692         :param verbosity Integer variable to store required verbosity level.
693         """
694         unittest.TestResult.__init__(self, stream, descriptions, verbosity)
695         self.stream = stream
696         self.descriptions = descriptions
697         self.verbosity = verbosity
698         self.result_string = None
699         self.printer = TestCasePrinter()
700
701     def addSuccess(self, test):
702         """
703         Record a test succeeded result
704
705         :param test:
706
707         """
708         if hasattr(test, 'logger'):
709             test.logger.debug("--- addSuccess() %s.%s(%s) called"
710                               % (test.__class__.__name__,
711                                  test._testMethodName,
712                                  test._testMethodDoc))
713         unittest.TestResult.addSuccess(self, test)
714         self.result_string = colorize("OK", GREEN)
715
716     def addSkip(self, test, reason):
717         """
718         Record a test skipped.
719
720         :param test:
721         :param reason:
722
723         """
724         if hasattr(test, 'logger'):
725             test.logger.debug("--- addSkip() %s.%s(%s) called, reason is %s"
726                               % (test.__class__.__name__,
727                                  test._testMethodName,
728                                  test._testMethodDoc,
729                                  reason))
730         unittest.TestResult.addSkip(self, test, reason)
731         self.result_string = colorize("SKIP", YELLOW)
732
733     def addFailure(self, test, err):
734         """
735         Record a test failed result
736
737         :param test:
738         :param err: error message
739
740         """
741         if hasattr(test, 'logger'):
742             test.logger.debug("--- addFailure() %s.%s(%s) called, err is %s"
743                               % (test.__class__.__name__,
744                                  test._testMethodName,
745                                  test._testMethodDoc, err))
746             test.logger.debug("formatted exception is:\n%s" %
747                               "".join(format_exception(*err)))
748         unittest.TestResult.addFailure(self, test, err)
749         if hasattr(test, 'tempdir'):
750             self.result_string = colorize("FAIL", RED) + \
751                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
752         else:
753             self.result_string = colorize("FAIL", RED) + ' [no temp dir]'
754
755     def addError(self, test, err):
756         """
757         Record a test error result
758
759         :param test:
760         :param err: error message
761
762         """
763         if hasattr(test, 'logger'):
764             test.logger.debug("--- addError() %s.%s(%s) called, err is %s"
765                               % (test.__class__.__name__,
766                                  test._testMethodName,
767                                  test._testMethodDoc, err))
768             test.logger.debug("formatted exception is:\n%s" %
769                               "".join(format_exception(*err)))
770         unittest.TestResult.addError(self, test, err)
771         if hasattr(test, 'tempdir'):
772             self.result_string = colorize("ERROR", RED) + \
773                 ' [ temp dir used by test case: ' + test.tempdir + ' ]'
774         else:
775             self.result_string = colorize("ERROR", RED) + ' [no temp dir]'
776
777     def getDescription(self, test):
778         """
779         Get test description
780
781         :param test:
782         :returns: test description
783
784         """
785         # TODO: if none print warning not raise exception
786         short_description = test.shortDescription()
787         if self.descriptions and short_description:
788             return short_description
789         else:
790             return str(test)
791
792     def startTest(self, test):
793         """
794         Start a test
795
796         :param test:
797
798         """
799         self.printer.print_test_case_heading_if_first_time(test)
800         unittest.TestResult.startTest(self, test)
801         if self.verbosity > 0:
802             self.stream.writeln(
803                 "Starting " + self.getDescription(test) + " ...")
804             self.stream.writeln(single_line_delim)
805
806     def stopTest(self, test):
807         """
808         Stop a test
809
810         :param test:
811
812         """
813         unittest.TestResult.stopTest(self, test)
814         if self.verbosity > 0:
815             self.stream.writeln(single_line_delim)
816             self.stream.writeln("%-73s%s" % (self.getDescription(test),
817                                              self.result_string))
818             self.stream.writeln(single_line_delim)
819         else:
820             self.stream.writeln("%-73s%s" % (self.getDescription(test),
821                                              self.result_string))
822
823     def printErrors(self):
824         """
825         Print errors from running the test case
826         """
827         self.stream.writeln()
828         self.printErrorList('ERROR', self.errors)
829         self.printErrorList('FAIL', self.failures)
830
831     def printErrorList(self, flavour, errors):
832         """
833         Print error list to the output stream together with error type
834         and test case description.
835
836         :param flavour: error type
837         :param errors: iterable errors
838
839         """
840         for test, err in errors:
841             self.stream.writeln(double_line_delim)
842             self.stream.writeln("%s: %s" %
843                                 (flavour, self.getDescription(test)))
844             self.stream.writeln(single_line_delim)
845             self.stream.writeln("%s" % err)
846
847
848 class VppTestRunner(unittest.TextTestRunner):
849     """
850     A basic test runner implementation which prints results to standard error.
851     """
852     @property
853     def resultclass(self):
854         """Class maintaining the results of the tests"""
855         return VppTestResult
856
857     def __init__(self, stream=sys.stderr, descriptions=True, verbosity=1,
858                  failfast=False, buffer=False, resultclass=None):
859         # ignore stream setting here, use hard-coded stdout to be in sync
860         # with prints from VppTestCase methods ...
861         super(VppTestRunner, self).__init__(sys.stdout, descriptions,
862                                             verbosity, failfast, buffer,
863                                             resultclass)
864
865     test_option = "TEST"
866
867     def parse_test_option(self):
868         try:
869             f = os.getenv(self.test_option)
870         except:
871             f = None
872         filter_file_name = None
873         filter_class_name = None
874         filter_func_name = None
875         if f:
876             if '.' in f:
877                 parts = f.split('.')
878                 if len(parts) > 3:
879                     raise Exception("Unrecognized %s option: %s" %
880                                     (self.test_option, f))
881                 if len(parts) > 2:
882                     if parts[2] not in ('*', ''):
883                         filter_func_name = parts[2]
884                 if parts[1] not in ('*', ''):
885                     filter_class_name = parts[1]
886                 if parts[0] not in ('*', ''):
887                     if parts[0].startswith('test_'):
888                         filter_file_name = parts[0]
889                     else:
890                         filter_file_name = 'test_%s' % parts[0]
891             else:
892                 if f.startswith('test_'):
893                     filter_file_name = f
894                 else:
895                     filter_file_name = 'test_%s' % f
896         return filter_file_name, filter_class_name, filter_func_name
897
898     def filter_tests(self, tests, filter_file, filter_class, filter_func):
899         result = unittest.suite.TestSuite()
900         for t in tests:
901             if isinstance(t, unittest.suite.TestSuite):
902                 # this is a bunch of tests, recursively filter...
903                 x = self.filter_tests(t, filter_file, filter_class,
904                                       filter_func)
905                 if x.countTestCases() > 0:
906                     result.addTest(x)
907             elif isinstance(t, unittest.TestCase):
908                 # this is a single test
909                 parts = t.id().split('.')
910                 # t.id() for common cases like this:
911                 # test_classifier.TestClassifier.test_acl_ip
912                 # apply filtering only if it is so
913                 if len(parts) == 3:
914                     if filter_file and filter_file != parts[0]:
915                         continue
916                     if filter_class and filter_class != parts[1]:
917                         continue
918                     if filter_func and filter_func != parts[2]:
919                         continue
920                 result.addTest(t)
921             else:
922                 # unexpected object, don't touch it
923                 result.addTest(t)
924         return result
925
926     def run(self, test):
927         """
928         Run the tests
929
930         :param test:
931
932         """
933         gc.disable()  # disable garbage collection, we'll do that manually
934         print("Running tests using custom test runner")  # debug message
935         filter_file, filter_class, filter_func = self.parse_test_option()
936         print("Active filters: file=%s, class=%s, function=%s" % (
937             filter_file, filter_class, filter_func))
938         filtered = self.filter_tests(test, filter_file, filter_class,
939                                      filter_func)
940         print("%s out of %s tests match specified filters" % (
941             filtered.countTestCases(), test.countTestCases()))
942         if not running_extended_tests():
943             print("Not running extended tests (some tests will be skipped)")
944         return super(VppTestRunner, self).run(filtered)