X-Git-Url: https://gerrit.fd.io/r/gitweb?a=blobdiff_plain;f=test%2Fframework.py;h=c1bfaa7630f54e14453fef780a88f613d676eb8e;hb=6ed154f779c765f38b61ed84349bd539287dd02b;hp=2bbb4ce44f7029a537cc0e1202406589e04da1d5;hpb=b8c72a4a8d8bd330ab62dc0c9461cac2b137575b;p=vpp.git diff --git a/test/framework.py b/test/framework.py index 2bbb4ce44f7..c1bfaa7630f 100644 --- a/test/framework.py +++ b/test/framework.py @@ -5,6 +5,7 @@ import gc import sys import os import select +import signal import unittest import tempfile import time @@ -12,18 +13,23 @@ import faulthandler import random import copy import psutil +import platform from collections import deque from threading import Thread, Event from inspect import getdoc, isclass from traceback import format_exception from logging import FileHandler, DEBUG, Formatter + +import scapy.compat from scapy.packet import Raw -from hook import StepHook, PollHook, VppDiedError +import hook as hookmodule from vpp_pg_interface import VppPGInterface from vpp_sub_interface import VppSubInterface from vpp_lo_interface import VppLoInterface +from vpp_bvi_interface import VppBviInterface from vpp_papi_provider import VppPapiProvider from vpp_papi.vpp_stats import VPPStats +from vpp_papi.vpp_transport_shmem import VppTransportShmemIOError from log import RED, GREEN, YELLOW, double_line_delim, single_line_delim, \ get_logger, colorize from vpp_object import VppObjectRegistry @@ -31,6 +37,7 @@ from util import ppp, is_core_present from scapy.layers.inet import IPerror, TCPerror, UDPerror, ICMPerror from scapy.layers.inet6 import ICMPv6DestUnreach, ICMPv6EchoRequest from scapy.layers.inet6 import ICMPv6EchoReply + if os.name == 'posix' and sys.version_info[0] < 3: # using subprocess32 is recommended by python official documentation # @ https://docs.python.org/2/library/subprocess.html @@ -38,6 +45,11 @@ if os.name == 'posix' and sys.version_info[0] < 3: else: import subprocess +# Python2/3 compatible +try: + input = raw_input +except NameError: + pass PASS = 0 FAIL = 1 @@ -46,12 +58,29 @@ SKIP = 3 TEST_RUN = 4 -debug_framework = False -if os.getenv('TEST_DEBUG', "0") == "1": - debug_framework = True - import debug_internal +class BoolEnvironmentVariable(object): + + def __init__(self, env_var_name, default='n', true_values=None): + self.name = env_var_name + self.default = default + self.true_values = true_values if true_values is not None else \ + ("y", "yes", "1") + + def __bool__(self): + return os.getenv(self.name, self.default).lower() in self.true_values + + if sys.version_info[0] == 2: + __nonzero__ = __bool__ + + def __repr__(self): + return 'BoolEnvironmentVariable(%r, default=%r, true_values=%r)' % \ + (self.name, self.default, self.true_values) +debug_framework = BoolEnvironmentVariable('TEST_DEBUG') +if debug_framework: + import debug_internal + """ Test framework module. @@ -60,6 +89,36 @@ if os.getenv('TEST_DEBUG', "0") == "1": """ +class VppDiedError(Exception): + """ exception for reporting that the subprocess has died.""" + + signals_by_value = {v: k for k, v in signal.__dict__.items() if + k.startswith('SIG') and not k.startswith('SIG_')} + + def __init__(self, rv=None, testcase=None, method_name=None): + self.rv = rv + self.signal_name = None + self.testcase = testcase + self.method_name = method_name + + try: + self.signal_name = VppDiedError.signals_by_value[-rv] + except (KeyError, TypeError): + pass + + if testcase is None and method_name is None: + in_msg = '' + else: + in_msg = 'running %s.%s ' % (testcase, method_name) + + msg = "VPP subprocess died %sunexpectedly with return code: %d%s." % ( + in_msg, + self.rv, + ' [%s]' % (self.signal_name if + self.signal_name is not None else '')) + super(VppDiedError, self).__init__(msg) + + class _PacketInfo(object): """Private class to create packet info object. @@ -116,7 +175,8 @@ def pump_output(testclass): if testclass.vpp.stderr.fileno() in readable: read = os.read(testclass.vpp.stderr.fileno(), 102400) if len(read) > 0: - split = read.splitlines(True) + split = read.decode('ascii', + errors='backslashreplace').splitlines(True) if len(stderr_fragment) > 0: split[0] = "%s%s" % (stderr_fragment, split[0]) if len(split) > 0 and split[-1].endswith("\n"): @@ -124,25 +184,45 @@ def pump_output(testclass): else: limit = -1 stderr_fragment = split[-1] + testclass.vpp_stderr_deque.extend(split[:limit]) if not testclass.cache_vpp_output: for line in split[:limit]: testclass.logger.debug( "VPP STDERR: %s" % line.rstrip("\n")) - # ignoring the dummy pipe here intentionally - the flag will take care - # of properly terminating the loop + # ignoring the dummy pipe here intentionally - the + # flag will take care of properly terminating the loop + + +def _is_skip_aarch64_set(): + return BoolEnvironmentVariable('SKIP_AARCH64') + + +is_skip_aarch64_set = _is_skip_aarch64_set() + +def _is_platform_aarch64(): + return platform.machine() == 'aarch64' -def running_extended_tests(): - s = os.getenv("EXTENDED_TESTS", "n") - return True if s.lower() in ("y", "yes", "1") else False +is_platform_aarch64 = _is_platform_aarch64() -def running_on_centos(): + +def _running_extended_tests(): + return BoolEnvironmentVariable("EXTENDED_TESTS") + + +running_extended_tests = _running_extended_tests() + + +def _running_on_centos(): os_id = os.getenv("OS_ID", "") return True if "centos" in os_id.lower() else False +running_on_centos = _running_on_centos() + + class KeepAliveReporter(object): """ Singleton object which reports test start to parent process @@ -184,6 +264,9 @@ class VppTestCase(unittest.TestCase): classes. It provides methods to create and run test case. """ + extra_vpp_punt_config = [] + extra_vpp_plugin_config = [] + @property def packet_infos(self): """List of packet infos""" @@ -248,25 +331,18 @@ class VppTestCase(unittest.TestCase): return random.choice(tuple(min_usage_set)) - @classmethod - def print_header(cls): - if not hasattr(cls, '_header_printed'): - print(double_line_delim) - print(colorize(getdoc(cls).splitlines()[0], GREEN)) - print(double_line_delim) - cls._header_printed = True - @classmethod def setUpConstants(cls): """ Set-up the test case class based on environment variables """ - s = os.getenv("STEP", "n") - cls.step = True if s.lower() in ("y", "yes", "1") else False + cls.step = BoolEnvironmentVariable('STEP') d = os.getenv("DEBUG", None) + # inverted case to handle '' == True c = os.getenv("CACHE_OUTPUT", "1") cls.cache_vpp_output = False if c.lower() in ("n", "no", "0") else True cls.set_debug_flags(d) cls.vpp_bin = os.getenv('VPP_BIN', "vpp") cls.plugin_path = os.getenv('VPP_PLUGIN_PATH') + cls.test_plugin_path = os.getenv('VPP_TEST_PLUGIN_PATH') cls.extern_plugin_path = os.getenv('EXTERN_PLUGINS') plugin_path = None if cls.plugin_path is not None: @@ -288,19 +364,30 @@ class VppTestCase(unittest.TestCase): coredump_size = "coredump-size unlimited" cpu_core_number = cls.get_least_used_cpu() + if not hasattr(cls, "worker_config"): + cls.worker_config = "" cls.vpp_cmdline = [cls.vpp_bin, "unix", "{", "nodaemon", debug_cli, "full-coredump", coredump_size, "runtime-dir", cls.tempdir, "}", "api-trace", "{", "on", "}", "api-segment", "{", "prefix", cls.shm_prefix, "}", "cpu", "{", - "main-core", str(cpu_core_number), "}", "statseg", - "{", "socket-name", cls.stats_sock, "}", "plugins", + "main-core", str(cpu_core_number), + cls.worker_config, "}", + "statseg", "{", "socket-name", cls.stats_sock, "}", + "socksvr", "{", "socket-name", cls.api_sock, "}", + "plugins", "{", "plugin", "dpdk_plugin.so", "{", "disable", + "}", "plugin", "rdma_plugin.so", "{", "disable", "}", "plugin", "unittest_plugin.so", "{", "enable", - "}", "}", ] + "}"] + cls.extra_vpp_plugin_config + ["}", ] + if cls.extra_vpp_punt_config is not None: + cls.vpp_cmdline.extend(cls.extra_vpp_punt_config) if plugin_path is not None: cls.vpp_cmdline.extend(["plugin_path", plugin_path]) + if cls.test_plugin_path is not None: + cls.vpp_cmdline.extend(["test_plugin_path", cls.test_plugin_path]) + cls.logger.info("vpp_cmdline args: %s" % cls.vpp_cmdline) cls.logger.info("vpp_cmdline: %s" % " ".join(cls.vpp_cmdline)) @@ -318,16 +405,17 @@ class VppTestCase(unittest.TestCase): print(single_line_delim) print("You can debug the VPP using e.g.:") if cls.debug_gdbserver: - print("gdb " + cls.vpp_bin + " -ex 'target remote localhost:7777'") + print("sudo gdb " + cls.vpp_bin + + " -ex 'target remote localhost:7777'") print("Now is the time to attach a gdb by running the above " "command, set up breakpoints etc. and then resume VPP from " "within gdb by issuing the 'continue' command") elif cls.debug_gdb: - print("gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid) + print("sudo gdb " + cls.vpp_bin + " -ex 'attach %s'" % cls.vpp.pid) print("Now is the time to attach a gdb by running the above " "command and set up breakpoints etc.") print(single_line_delim) - raw_input("Press ENTER to continue running the testcase...") + input("Press ENTER to continue running the testcase...") @classmethod def run_vpp(cls): @@ -349,46 +437,81 @@ class VppTestCase(unittest.TestCase): stderr=subprocess.PIPE, bufsize=1) except subprocess.CalledProcessError as e: - cls.logger.critical("Couldn't start vpp: %s" % e) + cls.logger.critical("Subprocess returned with non-0 return code: (" + "%s)", e.returncode) + raise + except OSError as e: + cls.logger.critical("Subprocess returned with OS error: " + "(%s) %s", e.errno, e.strerror) + raise + except Exception as e: + cls.logger.exception("Subprocess returned unexpected from " + "%s:", cmdline) raise cls.wait_for_enter() @classmethod def wait_for_stats_socket(cls): - deadline = time.time() + 3 + deadline = time.time() + 300 ok = False while time.time() < deadline or \ cls.debug_gdb or cls.debug_gdbserver: if os.path.exists(cls.stats_sock): ok = True break - time.sleep(0.8) + cls.sleep(0.8) if not ok: cls.logger.critical("Couldn't stat : {}".format(cls.stats_sock)) + @classmethod + def wait_for_coredump(cls): + corefile = cls.tempdir + "/core" + if os.path.isfile(corefile): + cls.logger.error("Waiting for coredump to complete: %s", corefile) + curr_size = os.path.getsize(corefile) + deadline = time.time() + 60 + ok = False + while time.time() < deadline: + cls.sleep(1) + size = curr_size + curr_size = os.path.getsize(corefile) + if size == curr_size: + ok = True + break + if not ok: + cls.logger.error("Timed out waiting for coredump to complete:" + " %s", corefile) + else: + cls.logger.error("Coredump complete: %s, size %d", + corefile, curr_size) + @classmethod def setUpClass(cls): """ Perform class setup before running the testcase Remove shared memory files, start vpp and connect the vpp-api """ + super(VppTestCase, cls).setUpClass() gc.collect() # run garbage collection first random.seed() - cls.print_header() cls.logger = get_logger(cls.__name__) if hasattr(cls, 'parallel_handler'): cls.logger.addHandler(cls.parallel_handler) cls.logger.propagate = False + cls.tempdir = tempfile.mkdtemp( prefix='vpp-unittest-%s-' % cls.__name__) cls.stats_sock = "%s/stats.sock" % cls.tempdir + cls.api_sock = "%s/api.sock" % cls.tempdir cls.file_handler = FileHandler("%s/log.txt" % cls.tempdir) cls.file_handler.setFormatter( Formatter(fmt='%(asctime)s,%(msecs)03d %(message)s', datefmt="%H:%M:%S")) cls.file_handler.setLevel(DEBUG) cls.logger.addHandler(cls.file_handler) + cls.logger.debug("--- setUpClass() for %s called ---" % + cls.__name__) cls.shm_prefix = os.path.basename(cls.tempdir) os.chdir(cls.tempdir) cls.logger.info("Temporary dir is %s, shm prefix is %s", @@ -396,7 +519,6 @@ class VppTestCase(unittest.TestCase): cls.setUpConstants() cls.reset_packet_infos() cls._captures = [] - cls._zombie_captures = [] cls.verbose = 0 cls.vpp_dead = False cls.registry = VppObjectRegistry() @@ -423,9 +545,9 @@ class VppTestCase(unittest.TestCase): cls.vapi = VppPapiProvider(cls.shm_prefix, cls.shm_prefix, cls, read_timeout) if cls.step: - hook = StepHook(cls) + hook = hookmodule.StepHook(cls) else: - hook = PollHook(cls) + hook = hookmodule.PollHook(cls) cls.vapi.register_hook(hook) cls.wait_for_stats_socket() cls.statistics = VPPStats(socketname=cls.stats_sock) @@ -449,11 +571,10 @@ class VppTestCase(unittest.TestCase): "VPP-API connection failed, did you forget " "to 'continue' VPP from within gdb?", RED)) raise - except Exception: - try: - cls.quit() - except Exception: - pass + except Exception as e: + cls.logger.debug("Exception connecting to VPP: %s" % e) + + cls.quit() raise @classmethod @@ -467,14 +588,14 @@ class VppTestCase(unittest.TestCase): print(double_line_delim) print("VPP or GDB server is still running") print(single_line_delim) - raw_input("When done debugging, press ENTER to kill the " - "process and finish running the testcase...") + input("When done debugging, press ENTER to kill the " + "process and finish running the testcase...") # first signal that we want to stop the pump thread, then wake it up if hasattr(cls, 'pump_thread_stop_flag'): cls.pump_thread_stop_flag.set() if hasattr(cls, 'pump_thread_wakeup_pipe'): - os.write(cls.pump_thread_wakeup_pipe[1], 'ding dong wake up') + os.write(cls.pump_thread_wakeup_pipe[1], b'ding dong wake up') if hasattr(cls, 'pump_thread'): cls.logger.debug("Waiting for pump thread to stop") cls.pump_thread.join() @@ -484,14 +605,21 @@ class VppTestCase(unittest.TestCase): if hasattr(cls, 'vpp'): if hasattr(cls, 'vapi'): + cls.logger.debug("Disconnecting class vapi client on %s", + cls.__name__) cls.vapi.disconnect() + cls.logger.debug("Deleting class vapi attribute on %s", + cls.__name__) del cls.vapi cls.vpp.poll() if cls.vpp.returncode is None: + cls.wait_for_coredump() cls.logger.debug("Sending TERM to vpp") - cls.vpp.kill() + cls.vpp.terminate() cls.logger.debug("Waiting for vpp to die") cls.vpp.communicate() + cls.logger.debug("Deleting class vpp attribute on %s", + cls.__name__) del cls.vpp if cls.vpp_startup_failed: @@ -524,6 +652,8 @@ class VppTestCase(unittest.TestCase): @classmethod def tearDownClass(cls): """ Perform final cleanup after running all tests in this test-case """ + cls.logger.debug("--- tearDownClass() for %s called ---" % + cls.__name__) cls.reporter.send_keep_alive(cls, 'tearDownClass') cls.quit() cls.file_handler.close() @@ -531,19 +661,28 @@ class VppTestCase(unittest.TestCase): if debug_framework: debug_internal.on_tear_down_class(cls) + def show_commands_at_teardown(self): + """ Allow subclass specific teardown logging additions.""" + self.logger.info("--- No test specific show commands provided. ---") + def tearDown(self): """ Show various debug prints after each test """ self.logger.debug("--- tearDown() for %s.%s(%s) called ---" % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)) - if not self.vpp_dead: - self.logger.debug(self.vapi.cli("show trace")) - self.logger.info(self.vapi.ppcli("show interface")) - self.logger.info(self.vapi.ppcli("show hardware")) - self.logger.info(self.statistics.set_errors_str()) - self.logger.info(self.vapi.ppcli("show run")) - self.logger.info(self.vapi.ppcli("show log")) - self.registry.remove_vpp_config(self.logger) + + try: + if not self.vpp_dead: + self.logger.debug(self.vapi.cli("show trace max 1000")) + self.logger.info(self.vapi.ppcli("show interface")) + self.logger.info(self.vapi.ppcli("show hardware")) + self.logger.info(self.statistics.set_errors_str()) + self.logger.info(self.vapi.ppcli("show run")) + self.logger.info(self.vapi.ppcli("show log")) + self.logger.info(self.vapi.ppcli("show bihash")) + self.logger.info("Logging testcase specific show commands.") + self.show_commands_at_teardown() + self.registry.remove_vpp_config(self.logger) # Save/Dump VPP api trace log api_trace = "vpp_api_trace.%s.log" % self._testMethodName tmp_api_trace = "/tmp/%s" % api_trace @@ -554,17 +693,21 @@ class VppTestCase(unittest.TestCase): os.rename(tmp_api_trace, vpp_api_trace_log) self.logger.info(self.vapi.ppcli("api trace custom-dump %s" % vpp_api_trace_log)) + except VppTransportShmemIOError: + self.logger.debug("VppTransportShmemIOError: Vpp dead. " + "Cannot log show commands.") + self.vpp_dead = True else: self.registry.unregister_all(self.logger) def setUp(self): """ Clear trace before running each test""" + super(VppTestCase, self).setUp() self.reporter.send_keep_alive(self) - self.logger.debug("--- setUp() for %s.%s(%s) called ---" % - (self.__class__.__name__, self._testMethodName, - self._testMethodDoc)) if self.vpp_dead: - raise Exception("VPP is dead when setting up the test") + + raise VppDiedError(rv=None, testcase=self.__class__.__name__, + method_name=self._testMethodName) self.sleep(.1, "during setUp") self.vpp_stdout_deque.append( "--- test setUp() for %s.%s(%s) starts here ---\n" % @@ -598,33 +741,28 @@ class VppTestCase(unittest.TestCase): """ Register a capture in the testclass """ # add to the list of captures with current timestamp cls._captures.append((time.time(), cap_name)) - # filter out from zombies - cls._zombie_captures = [(stamp, name) - for (stamp, name) in cls._zombie_captures - if name != cap_name] @classmethod def pg_start(cls): - """ Remove any zombie captures and enable the packet generator """ - # how long before capture is allowed to be deleted - otherwise vpp - # crashes - 100ms seems enough (this shouldn't be needed at all) - capture_ttl = 0.1 - now = time.time() - for stamp, cap_name in cls._zombie_captures: - wait = stamp + capture_ttl - now - if wait > 0: - cls.sleep(wait, "before deleting capture %s" % cap_name) - now = time.time() - cls.logger.debug("Removing zombie capture %s" % cap_name) - cls.vapi.cli('packet-generator delete %s' % cap_name) - - cls.vapi.cli("trace add pg-input 50") # 50 is maximum + """ Enable the PG, wait till it is done, then clean up """ + cls.vapi.cli("trace add pg-input 1000") cls.vapi.cli('packet-generator enable') - cls._zombie_captures = cls._captures + # PG, when starts, runs to completion - + # so let's avoid a race condition, + # and wait a little till it's done. + # Then clean it up - and then be gone. + deadline = time.time() + 300 + while cls.vapi.cli('show packet-generator').find("Yes") != -1: + cls.sleep(0.01) # yield + if time.time() > deadline: + cls.logger.error("Timeout waiting for pg to stop") + break + for stamp, cap_name in cls._captures: + cls.vapi.cli('packet-generator delete %s' % cap_name) cls._captures = [] @classmethod - def create_pg_interfaces(cls, interfaces): + def create_pg_interfaces(cls, interfaces, gso=0, gso_size=0): """ Create packet-generator interfaces. @@ -634,7 +772,7 @@ class VppTestCase(unittest.TestCase): """ result = [] for i in interfaces: - intf = VppPGInterface(cls, i) + intf = VppPGInterface(cls, i, gso, gso_size) setattr(cls, intf.name, intf) result.append(intf) cls.pg_interfaces = result @@ -654,6 +792,20 @@ class VppTestCase(unittest.TestCase): cls.lo_interfaces = result return result + @classmethod + def create_bvi_interfaces(cls, count): + """ + Create BVI interfaces. + + :param count: number of interfaces created. + :returns: List of created interfaces. + """ + result = [VppBviInterface(cls) for i in range(count)] + for intf in result: + setattr(cls, intf.name, intf) + cls.bvi_interfaces = result + return result + @staticmethod def extend_packet(packet, size, padding=' '): """ @@ -668,8 +820,8 @@ class VppTestCase(unittest.TestCase): packet_len = len(packet) + 4 extend = size - packet_len if extend > 0: - num = (extend / len(padding)) + 1 - packet[Raw].load += (padding * num)[:extend] + num = (extend // len(padding)) + 1 + packet[Raw].load += (padding * num)[:extend].encode("ascii") @classmethod def reset_packet_infos(cls): @@ -717,16 +869,19 @@ class VppTestCase(unittest.TestCase): info.ip, info.proto) @staticmethod - def payload_to_info(payload): + def payload_to_info(payload, payload_field='load'): """ Convert packet payload to _PacketInfo object :param payload: packet payload - + :type payload: + :param payload_field: packet fieldname of payload "load" for + + :type payload_field: str :returns: _PacketInfo object containing de-serialized data from payload """ - numbers = payload.split() + numbers = getattr(payload, payload_field).split() info = _PacketInfo() info.index = int(numbers[0]) info.src = int(numbers[1]) @@ -817,14 +972,12 @@ class VppTestCase(unittest.TestCase): def assert_packet_checksums_valid(self, packet, ignore_zero_udp_checksums=True): - received = packet.__class__(str(packet)) - self.logger.debug( - ppp("Verifying packet checksums for packet:", received)) + received = packet.__class__(scapy.compat.raw(packet)) udp_layers = ['UDP', 'UDPerror'] checksum_fields = ['cksum', 'chksum'] checksums = [] counter = 0 - temp = received.__class__(str(received)) + temp = received.__class__(scapy.compat.raw(received)) while True: layer = temp.getlayer(counter) if layer: @@ -841,7 +994,7 @@ class VppTestCase(unittest.TestCase): counter = counter + 1 if 0 == len(checksums): return - temp = temp.__class__(str(temp)) + temp = temp.__class__(scapy.compat.raw(temp)) for layer, cf in checksums: calc_sum = getattr(temp[layer], cf) self.assert_equal( @@ -858,9 +1011,10 @@ class VppTestCase(unittest.TestCase): received_packet_checksum = getattr(received_packet[layer], field_name) if ignore_zero_checksum and 0 == received_packet_checksum: return - recalculated = received_packet.__class__(str(received_packet)) + recalculated = received_packet.__class__( + scapy.compat.raw(received_packet)) delattr(recalculated[layer], field_name) - recalculated = recalculated.__class__(str(recalculated)) + recalculated = recalculated.__class__(scapy.compat.raw(recalculated)) self.assert_equal(received_packet_checksum, getattr(recalculated[layer], field_name), "packet checksum on layer: %s" % layer) @@ -904,38 +1058,66 @@ class VppTestCase(unittest.TestCase): if pkt.haslayer(ICMPv6EchoReply): self.assert_checksum_valid(pkt, 'ICMPv6EchoReply', 'cksum') + def get_packet_counter(self, counter): + if counter.startswith("/"): + counter_value = self.statistics.get_counter(counter) + else: + counters = self.vapi.cli("sh errors").split('\n') + counter_value = 0 + for i in range(1, len(counters) - 1): + results = counters[i].split() + if results[1] == counter: + counter_value = int(results[0]) + break + return counter_value + def assert_packet_counter_equal(self, counter, expected_value): - counters = self.vapi.cli("sh errors").split('\n') - counter_value = -1 - for i in range(1, len(counters)-1): - results = counters[i].split() - if results[1] == counter: - counter_value = int(results[0]) - break + counter_value = self.get_packet_counter(counter) self.assert_equal(counter_value, expected_value, "packet counter `%s'" % counter) + def assert_error_counter_equal(self, counter, expected_value): + counter_value = self.statistics.get_err_counter(counter) + self.assert_equal(counter_value, expected_value, + "error counter `%s'" % counter) + @classmethod def sleep(cls, timeout, remark=None): + + # /* Allow sleep(0) to maintain win32 semantics, and as decreed + # * by Guido, only the main thread can be interrupted. + # */ + # https://github.com/python/cpython/blob/6673decfa0fb078f60587f5cb5e98460eea137c2/Modules/timemodule.c#L1892 # noqa + if timeout == 0: + # yield quantum + if hasattr(os, 'sched_yield'): + os.sched_yield() + else: + time.sleep(0) + return + if hasattr(cls, 'logger'): - cls.logger.debug("Starting sleep for %ss (%s)" % (timeout, remark)) + cls.logger.debug("Starting sleep for %es (%s)", timeout, remark) before = time.time() time.sleep(timeout) after = time.time() - if after - before > 2 * timeout: - cls.logger.error("unexpected time.sleep() result - " - "slept for %ss instead of ~%ss!" % ( - after - before, timeout)) + if hasattr(cls, 'logger') and after - before > 2 * timeout: + cls.logger.error("unexpected self.sleep() result - " + "slept for %es instead of ~%es!", + after - before, timeout) if hasattr(cls, 'logger'): cls.logger.debug( - "Finished sleep (%s) - slept %ss (wanted %ss)" % ( - remark, after - before, timeout)) + "Finished sleep (%s) - slept %es (wanted %es)", + remark, after - before, timeout) - def send_and_assert_no_replies(self, intf, pkts, remark="", timeout=None): + def pg_send(self, intf, pkts): self.vapi.cli("clear trace") intf.add_stream(pkts) self.pg_enable_capture(self.pg_interfaces) self.pg_start() + + def send_and_assert_no_replies(self, intf, pkts, remark="", timeout=None): + self.pg_send(intf, pkts) if not timeout: timeout = 1 for i in self.pg_interfaces: @@ -943,32 +1125,17 @@ class VppTestCase(unittest.TestCase): i.assert_nothing_captured(remark=remark) timeout = 0.1 - def send_and_expect(self, input, pkts, output): - self.vapi.cli("clear trace") - input.add_stream(pkts) - self.pg_enable_capture(self.pg_interfaces) - self.pg_start() - if isinstance(object, (list,)): - rx = [] - for o in output: - rx.append(output.get_capture(len(pkts))) - else: - rx = output.get_capture(len(pkts)) + def send_and_expect(self, intf, pkts, output, n_rx=None): + if not n_rx: + n_rx = len(pkts) + self.pg_send(intf, pkts) + rx = output.get_capture(n_rx) return rx - def send_and_expect_only(self, input, pkts, output, timeout=None): - self.vapi.cli("clear trace") - input.add_stream(pkts) - self.pg_enable_capture(self.pg_interfaces) - self.pg_start() - if isinstance(object, (list,)): - outputs = output - rx = [] - for o in outputs: - rx.append(output.get_capture(len(pkts))) - else: - rx = output.get_capture(len(pkts)) - outputs = [output] + def send_and_expect_only(self, intf, pkts, output, timeout=None): + self.pg_send(intf, pkts) + rx = output.get_capture(len(pkts)) + outputs = [output] if not timeout: timeout = 1 for i in self.pg_interfaces: @@ -979,6 +1146,11 @@ class VppTestCase(unittest.TestCase): return rx + def runTest(self): + """ unittest calls runTest when TestCase is instantiated without a + test case. Use case: Writing unittests against VppTestCase""" + pass + def get_testcase_doc_name(test): return getdoc(test.__class__).splitlines()[0] @@ -1020,7 +1192,8 @@ class VppTestResult(unittest.TestResult): core_crash_test_cases_info = set() current_test_case_info = None - def __init__(self, stream, descriptions, verbosity, runner): + def __init__(self, stream=None, descriptions=None, verbosity=None, + runner=None): """ :param stream File descriptor to store where to report test results. Set to the standard error stream by default. @@ -1028,7 +1201,7 @@ class VppTestResult(unittest.TestResult): test case descriptions. :param verbosity Integer variable to store required verbosity level. """ - unittest.TestResult.__init__(self, stream, descriptions, verbosity) + super(VppTestResult, self).__init__(stream, descriptions, verbosity) self.stream = stream self.descriptions = descriptions self.verbosity = verbosity @@ -1139,7 +1312,7 @@ class VppTestResult(unittest.TestResult): if isinstance(test, unittest.suite._ErrorHolder): test_name = str(test) else: - test_name = "'{}' ({})".format( + test_name = "'{!s}' ({!s})".format( get_testcase_doc_name(test), test.id()) self.current_test_case_info.core_crash_test = test_name self.core_crash_test_cases_info.add( @@ -1186,7 +1359,15 @@ class VppTestResult(unittest.TestResult): :param test: """ - test.print_header() + + def print_header(test): + if not hasattr(test.__class__, '_header_printed'): + print(double_line_delim) + print(colorize(getdoc(test).splitlines()[0], GREEN)) + print(double_line_delim) + test.__class__._header_printed = True + + print_header(test) unittest.TestResult.startTest(self, test) if self.verbosity > 0: @@ -1249,6 +1430,7 @@ class VppTestRunner(unittest.TextTestRunner): """ A basic test runner implementation which prints results to standard error. """ + @property def resultclass(self): """Class maintaining the results of the tests""" @@ -1256,13 +1438,12 @@ class VppTestRunner(unittest.TextTestRunner): def __init__(self, keep_alive_pipe=None, descriptions=True, verbosity=1, result_pipe=None, failfast=False, buffer=False, - resultclass=None, print_summary=True): - + resultclass=None, print_summary=True, **kwargs): # ignore stream setting here, use hard-coded stdout to be in sync # with prints from VppTestCase methods ... super(VppTestRunner, self).__init__(sys.stdout, descriptions, verbosity, failfast, buffer, - resultclass) + resultclass, **kwargs) KeepAliveReporter.pipe = keep_alive_pipe self.orig_stream = self.stream @@ -1293,15 +1474,24 @@ class VppTestRunner(unittest.TextTestRunner): class Worker(Thread): - def __init__(self, args, logger, env={}): + def __init__(self, args, logger, env=None): self.logger = logger self.args = args + self.process = None self.result = None + env = {} if env is None else env self.env = copy.deepcopy(env) super(Worker, self).__init__() def run(self): executable = self.args[0] + if not os.path.exists(executable) or not os.access( + executable, os.F_OK | os.X_OK): + # Exit code that means some system file did not exist, + # could not be opened, or had some other kind of error. + self.result = os.EX_OSFILE + raise EnvironmentError( + "executable '%s' is not found or executable." % executable) self.logger.debug("Running executable w/args `%s'" % self.args) env = os.environ.copy() env.update(self.env) @@ -1322,3 +1512,7 @@ class Worker(Thread): self.logger.info(err) self.logger.info(single_line_delim) self.result = self.process.returncode + + +if __name__ == '__main__': + pass