aboutsummaryrefslogtreecommitdiffstats
path: root/vppapigen
AgeCommit message (Collapse)AuthorFilesLines
2016-12-02API: Packaging of JSON files.Ole Troan2-292/+1
Change-Id: If041b6faf1a091d4758b514f0a8cd800ee0e6a89 Signed-off-by: Ole Troan <ot@cisco.com> Signed-off-by: Ole Troan <otroan@ot-vpp.cisco.com> Signed-off-by: Ole Troan <ot@cisco.com>
2016-11-23Fix coverity warnings, VPP-486Dave Barach1-0/+1
Change-Id: Id88805f77642187f304f8baef1f6b447728e4b5b Signed-off-by: Dave Barach <dave@barachs.net>
2016-11-21Add client-side msg_name_and_crc -> msg_index tableDave Barach4-37/+219
vppapigen now generates per-message crcs. Verified that whitespace and real changes in message A don't change the crc for message B, etc. Fixed the sample and flowperpkt plugins to participate. Others need the same treatment. They don't build due to python/java language binding build issues. To use the scheme: Client connects as usual. Then call: u32 vl_api_get_msg_index(char * name_and_crc) name_and_crc is a string like: "flowperpkt_tx_interface_add_del_753301f3", aka the message name with _%08x <expected crc> appended. Try these vpp-api-test commands to play with it: vat# dump_msg_api_table <snip> [366]: punt_reply_cca27fbe [367]: ipsec_spd_dump_5e9ae88e [368]: ipsec_spd_details_6f7821b0 [369]: sample_macswap_enable_disable_0f2813e2 [370]: sample_macswap_enable_disable_reply_476738e5 [371]: flowperpkt_tx_interface_add_del_753301f3 [372]: flowperpkt_tx_interface_add_del_reply_d47e6e0b vat# get_msg_id sample_macswap_enable_disable_reply_476738e5 'sample_macswap_enable_disable_reply_476738e5' has message index 370 vat# get_msg_id sample_macswap_enable_disable_reply_476738e3 'sample_macswap_enable_disable_reply_476738e3' not found CRCs may vary, etc. vppapigen is used to build a set of JSON representations of each API file from vpp-api/Makefile.am and that is in turn used by each language binding (Java, Python, Lua). Change-Id: I3d64582e779dac5f20cddec79c562c288d8fd9c6 Signed-off-by: Dave Barach <dave@barachs.net> Signed-off-by: Ole Troan <ot@cisco.com>
2016-11-15Dump routes (VPP-500)Steven1-0/+2
Change-Id: I86e7382395a8b6471a0deaf57163718d41b71b83 Signed-off-by: Steven <sluong@cisco.com>
2016-10-26python api - vla - allow the user to pass in a normal python listGabriel Ganne1-2/+11
Only add support for named variable-length-arrays as some api fo not follow the expected order of length preceding the value. Change-Id: I4c22c9b3c05f23edb3edc1cbc445be87b0a69162 Signed-off-by: Gabriel Ganne <gabriel.ganne@qosmos.com>
2016-10-12VPP-362 Implement dumping of LISP adjacenciesFilip Tehlar1-0/+2
Change-Id: Ieea56f3bf9e749878d9f2b35d39d9f7a9cdabde4 Signed-off-by: Filip Tehlar <ftehlar@cisco.com>
2016-09-30VPP-120: include custom types to Python representation of vpe.apiMarek Gradzki2-20/+42
Change-Id: Ide97a8bf55d3baf41a1e86af2c67c6b7b26b657a Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
2016-09-21vpp python api - fix vla pack generationGabriel Ganne1-20/+15
This fixes commit 1732fc1ab851c454a74efda47a383f48691d545a. Change-Id: I3d9f1384c20d2ca22eaf2aae553455789b934b6e Signed-off-by: Gabriel Ganne <gabriel.ganne@qosmos.com>
2016-09-13Python API: Re-adding rudimentary variable length array pack support.Ole Troan1-7/+19
Fixed bug in message-id mapping with non-consequtive APIs. Change-Id: Icd6073e4655f7ce5432816861ae58915e5b336af Signed-off-by: Ole Troan <ot@cisco.com>
2016-08-25VPP Python language binding - plugin supportOle Troan4-2/+283
- Moved Python generator tool to tools directory - Added build-vpp-api Makefile target - Generator now only creates a Python representation of the .api the rest of the framework is in the vpp_papi script - Each plugin has its own namespace. - Plugin Python files are installed in vpp_papi_plugins for easy use inside the build tree. Change-Id: I272c83bb7e5d5e416bdbd8a790a3cc35c5a04e38 Signed-off-by: Ole Troan <ot@cisco.com>
2016-08-05vpp-189 Clean up more coverity warningsDave Barach2-28/+31
Time to make the donuts Change-Id: I528937800f7daefce19723dda0216e58d857942c Signed-off-by: Dave Barach <dave@barachs.net>
2016-07-22VPP-189 Fix 137 coverity no-op issues from vpeapigenChris Luke1-2/+12
vpeapigen is not properly detecting no-op endian swap operations (on u8 or u8[] types) which in turn causes Coverity to currently report ~137 minor issues from the generated vpe.api.h file. This patch makes vpeapigen comment out those no-op situations thus: /* a->interface_name[0..63] = a->interface_name[0..63] (no-op) */ /* a->admin_up_down = a->admin_up_down (no-op) */ Change-Id: Ifbc5c93e1939206656e0515b528a4d167c539fd2 Signed-off-by: Chris Luke <chrisy@flirble.org>
2016-07-21VPP-123: remove japi (the old Java API)Marek Gradzki4-837/+10
MANUAL_JAVA flag (used only by the japi) was also removed. Change-Id: Ied21521b2410af1c357afb04cbf9e849632ddc5f Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
2016-07-08Remove unnecessary and obsolete configure.ac directivesDamjan Marion1-1/+1
Also enable silent rules where missing Change-Id: Ia521886815c862b013f01df4cc18fd8a298aaaa1 Signed-off-by: Damjan Marion <damarion@cisco.com>
2016-06-27More janitorial workDave Barach1-5/+5
Install vpp api header files in /usr/include/vpp-api, instead of /usr/include/api. Someone will eventually complain if we continue with the status quo. Generate /usr/bin/vpp_plugin_configure, to correctly configure standalone plugin compilation against header files installed from the dev package. If a plugin's CFLAGS don't precisely match the installed vpp engine binary, subtle misbehavior can and will occur. Example: the ip4/ip6 main_t structure size / member offsets depend on DPDK=[0|1]. Screw that one up, and your brand-new configurable ip feature will mysteriously fail to appear, even though the plugin loads perfectly. Change-Id: I20c97fe1042808a79935863209d995c31953b98c Signed-off-by: Dave Barach <dave@barachs.net>
2016-06-24VPP-118: add support for variable length arrays to jvppMarek Gradzki3-2/+24
* extends VPP's message definition language with the following syntax: u32 count: u8 array[count]; which is traslated to: u32 count; u8 array[0]; but now, python API representation generated by vppapigen contains information about where the array length is stored. * modifies existing response messages to use the new syntax Change-Id: I68210bc7a3a755d03d067e9b79a567f40e2d31f3 Signed-off-by: Marek Gradzki <mgradzki@cisco.com>
2016-04-21Fix warning in vppapigenDamjan Marion1-1/+1
Change-Id: I6301ece0d772fd5a6271edd07765b05ef0b63f21 Signed-off-by: Damjan Marion <damarion@cisco.com>
2016-04-20Python-API: Inital commit of Python bindings for the VPP API.Ole Troan3-4/+89
See: https://wiki.fd.io/view/VPP/Python_API Change-Id: If135fc32208c7031787e1935b399d930e0e1ea1f Signed-off-by: Ole Troan <ot@cisco.com>
2016-04-18Add support for AArch32Christophe Fontaine1-2/+2
gcc version 4.9.2 (Raspbian 4.9.2-10) Tested on Linux raspberrypi 4.4.6-v7+ #875 SMP Tue Apr 12 16:33:02 BST 2016 armv7l GNU/Linux CPUs may be little or big endian, detect with gcc flags, not the processor architecture Add a new flag $(PLATFORM)_uses_openssl which allows to disable the link with openssl lib. vlib/vlib/threads.c: startup.conf must: - specify the heapsize as we don't have hugepages on raspbian cpu { main-core 3 } heapsize 64M Corrects in various files the assumption uword == u64 and replaces 'u64' cast with 'pointer_to_uword' and 'uword_to_pointer' where appropriate. 256 CPUs may create an OOM when testing with small memory footprint ( heapsize 64M ), allows the number of VLIB_MAX_CPUS to be set in platforms/*.mk vppinfra/vppinfra/longjmp.S: ARM - copy r1 (1st parameter of the setjmp call) to r0 (return value) vppinfra/vppinfra/time.h: On ARMv7 in AArch32 mode, we can access to a 64bit register to retreive the cycles count. gcc on rpi only declare ARM_ARCH 6. Override this info, and check if it is possible to use 'mrrc'. /!\ the time function will NOT work without allowing the user mode access to the PMU. You may download the source of the kmod here: https://github.com/christophefontaine/arm_rdtsc Change-Id: I8142606436d9671a184133b935398427f08a8bd2 Signed-off-by: Christophe Fontaine <christophe.fontaine@qosmos.com>
2016-03-14lex - yylval undeclaredVincent JARDIN1-0/+1
Fix compilation with Ubuntu 16.04/yacc: $ yacc -V yacc - 1.9 20140715 $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu Xenial Xerus (development branch) Release: 16.04 Codename: xenial Change-Id: Ib1a6d7b7f9485db893465041ac6bce2a98ded2f7 Signed-off-by: Vincent JARDIN <vincent.jardin@6wind.com>
2016-02-01Refactor vpp-japiRobert Varga1-46/+72
vlib does not allow concurrent connections, hence make sure the Java API does not allow the user to instantiate them. Also hide native methods from direct user invocation behind defensive methods also detect if a particular connection has been terminated. Disconnect is hidden befind standard close() method, as specified by AutoCloseable. Change-Id: Ib5079200ae4216cad84358a2174a41e90271a30b Signed-off-by: Robert Varga <nite@hq.sk>
2015-12-23ylwrap is also autotools autogenerated fileDamjan Marion1-247/+0
Change-Id: Ia6f26b5a12c2fb7847f9325519a99cf928263374 Signed-off-by: Damjan Marion <damarion@cisco.com>
2015-12-15Remove autotools generated productsDave Barach7-8613/+0
Change-Id: I7f23b8b8e5136cb56768bac3a7473e6df5ee4993 Signed-off-by: Dave Barach <dave@barachs.net>
2015-12-08Initial commit of vpp code.v1.0.0Ed Warnicke15-0/+12200
Change-Id: Ib246f1fbfce93274020ee93ce461e3d8bd8b9f17 Signed-off-by: Ed Warnicke <eaw@cisco.com>
ghlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
#!/usr/bin/env python

import sys
import shutil
import os
import select
import unittest
import argparse
import time
import threading
import signal
import psutil
from multiprocessing import Process, Pipe, cpu_count
from multiprocessing.queues import Queue
from multiprocessing.managers import BaseManager
from framework import VppTestRunner, running_extended_tests, VppTestCase, \
    get_testcase_doc_name, get_test_description
from debug import spawn_gdb
from log import get_parallel_logger, double_line_delim, RED, YELLOW, GREEN, \
    colorize
from discover_tests import discover_tests
from subprocess import check_output, CalledProcessError
from util import check_core_path

# timeout which controls how long the child has to finish after seeing
# a core dump in test temporary directory. If this is exceeded, parent assumes
# that child process is stuck (e.g. waiting for shm mutex, which will never
# get unlocked) and kill the child
core_timeout = 3
min_req_shm = 536870912  # min 512MB shm required
# 128MB per extra process
shm_per_process = 134217728


class StreamQueue(Queue):
    def write(self, msg):
        self.put(msg)

    def flush(self):
        sys.__stdout__.flush()
        sys.__stderr__.flush()

    def fileno(self):
        return self._writer.fileno()


class StreamQueueManager(BaseManager):
    pass


StreamQueueManager.register('Queue', StreamQueue)


def test_runner_wrapper(suite, keep_alive_pipe, result_pipe, stdouterr_queue,
                        partial_result_queue, logger):
    sys.stdout = stdouterr_queue
    sys.stderr = stdouterr_queue
    VppTestCase.logger = logger
    result = VppTestRunner(keep_alive_pipe=keep_alive_pipe,
                           descriptions=descriptions,
                           verbosity=verbose,
                           results_pipe=partial_result_queue,
                           failfast=failfast).run(suite)
    result_pipe.send(result)
    result_pipe.close()
    keep_alive_pipe.close()


class TestCaseWrapper(object):
    def __init__(self, testcase_suite, manager):
        self.keep_alive_parent_end, self.keep_alive_child_end = Pipe(
            duplex=False)
        self.result_parent_end, self.result_child_end = Pipe(duplex=False)
        self.partial_result_parent_end, self.partial_result_child_end = Pipe(
            duplex=False)
        self.testcase_suite = testcase_suite
        self.stdouterr_queue = manager.Queue()
        self.logger = get_parallel_logger(self.stdouterr_queue)
        self.child = Process(target=test_runner_wrapper,
                             args=(testcase_suite, self.keep_alive_child_end,
                                   self.result_child_end, self.stdouterr_queue,
                                   self.partial_result_child_end, self.logger)
                             )
        self.child.start()
        self.pid = self.child.pid
        self.last_test_temp_dir = None
        self.last_test_vpp_binary = None
        self.last_test = None
        self.result = None
        self.last_heard = time.time()
        self.core_detected_at = None
        self.failed_tests = []
        self.partial_result = None

    def close_pipes(self):
        self.keep_alive_child_end.close()
        self.result_child_end.close()
        self.partial_result_child_end.close()
        self.keep_alive_parent_end.close()
        self.result_parent_end.close()
        self.partial_result_parent_end.close()


def stdouterr_reader_wrapper(unread_testcases, finished_unread_testcases,
                             read_testcases):
    read_testcase = None
    while read_testcases.is_set() or len(unread_testcases) > 0:
        if not read_testcase:
            if len(finished_unread_testcases) > 0:
                read_testcase = finished_unread_testcases.pop()
                unread_testcases.remove(read_testcase)
            elif len(unread_testcases) > 0:
                read_testcase = unread_testcases.pop()
        if read_testcase:
            data = ''
            while data is not None:
                sys.stdout.write(data)
                data = read_testcase.stdouterr_queue.get()

            read_testcase.stdouterr_queue.close()
            finished_unread_testcases.discard(read_testcase)
            read_testcase = None


def run_forked(testcases):
    wrapped_testcase_suites = set()

    # suites are unhashable, need to use list
    results = []
    debug_core = os.getenv("DEBUG", "").lower() == "core"
    unread_testcases = set()
    finished_unread_testcases = set()
    manager = StreamQueueManager()
    manager.start()
    for i in range(concurrent_tests):
        if len(testcases) > 0:
            wrapped_testcase_suite = TestCaseWrapper(testcases.pop(0), manager)
            wrapped_testcase_suites.add(wrapped_testcase_suite)
            unread_testcases.add(wrapped_testcase_suite)
            # time.sleep(1)
        else:
            break

    read_from_testcases = threading.Event()
    read_from_testcases.set()
    stdouterr_thread = threading.Thread(target=stdouterr_reader_wrapper,
                                        args=(unread_testcases,
                                              finished_unread_testcases,
                                              read_from_testcases))
    stdouterr_thread.start()

    while len(wrapped_testcase_suites) > 0:
        finished_testcase_suites = set()
        for wrapped_testcase_suite in wrapped_testcase_suites:
            readable = select.select(
                [wrapped_testcase_suite.keep_alive_parent_end.fileno(),
                 wrapped_testcase_suite.result_parent_end.fileno(),
                 wrapped_testcase_suite.partial_result_parent_end.fileno()],
                [], [], 1)[0]
            if wrapped_testcase_suite.result_parent_end.fileno() in readable:
                results.append(
                    (wrapped_testcase_suite.testcase_suite,
                     wrapped_testcase_suite.result_parent_end.recv()))
                finished_testcase_suites.add(wrapped_testcase_suite)
                continue

            if wrapped_testcase_suite.partial_result_parent_end.fileno() \
                    in readable:
                while wrapped_testcase_suite.partial_result_parent_end.poll():
                    wrapped_testcase_suite.partial_result = \
                        wrapped_testcase_suite.partial_result_parent_end.recv()
                    wrapped_testcase_suite.last_heard = time.time()

            if wrapped_testcase_suite.keep_alive_parent_end.fileno() \
                    in readable:
                while wrapped_testcase_suite.keep_alive_parent_end.poll():
                    wrapped_testcase_suite.last_test, \
                        wrapped_testcase_suite.last_test_vpp_binary, \
                        wrapped_testcase_suite.last_test_temp_dir, \
                        wrapped_testcase_suite.vpp_pid = \
                        wrapped_testcase_suite.keep_alive_parent_end.recv()
                wrapped_testcase_suite.last_heard = time.time()

            fail = False
            if wrapped_testcase_suite.last_heard + test_timeout < time.time() \
                    and not os.path.isfile(
                                "%s/_core_handled" %
                                wrapped_testcase_suite.last_test_temp_dir):
                fail = True
                wrapped_testcase_suite.logger.critical(
                    "Timeout while waiting for child test "
                    "runner process (last test running was "
                    "`%s' in `%s')!" %
                    (wrapped_testcase_suite.last_test,
                     wrapped_testcase_suite.last_test_temp_dir))
            elif not wrapped_testcase_suite.child.is_alive():
                fail = True
                wrapped_testcase_suite.logger.critical(
                    "Child python process unexpectedly died "
                    "(last test running was `%s' in `%s')!" %
                    (wrapped_testcase_suite.last_test,
                     wrapped_testcase_suite.last_test_temp_dir))
            elif wrapped_testcase_suite.last_test_temp_dir and \
                    wrapped_testcase_suite.last_test_vpp_binary:
                core_path = "%s/core" % \
                            wrapped_testcase_suite.last_test_temp_dir
                if os.path.isfile(core_path):
                    if wrapped_testcase_suite.core_detected_at is None:
                        wrapped_testcase_suite.core_detected_at = time.time()
                    elif wrapped_testcase_suite.core_detected_at + \
                            core_timeout < time.time():
                        if not os.path.isfile(
                                        "%s/_core_handled" %
                                        wrapped_testcase_suite.
                                        last_test_temp_dir):
                            wrapped_testcase_suite.logger.critical(
                                "Child python process unresponsive and core-"
                                "file exists in test temporary directory!")
                            fail = True

            if fail:
                failed_dir = os.getenv('VPP_TEST_FAILED_DIR')
                lttd = os.path.basename(
                    wrapped_testcase_suite.last_test_temp_dir)
                link_path = '%s%s-FAILED' % (failed_dir, lttd)
                wrapped_testcase_suite.logger.error(
                    "Creating a link to the failed test: %s -> %s" %
                    (link_path, lttd))
                if not os.path.exists(link_path):
                    os.symlink(wrapped_testcase_suite.last_test_temp_dir,
                               link_path)
                api_post_mortem_path = "/tmp/api_post_mortem.%d" % \
                                       wrapped_testcase_suite.vpp_pid
                if os.path.isfile(api_post_mortem_path):
                    wrapped_testcase_suite.logger.error(
                        "Copying api_post_mortem.%d to %s" %
                        (wrapped_testcase_suite.vpp_pid,
                         wrapped_testcase_suite.last_test_temp_dir))
                    shutil.copy2(api_post_mortem_path,
                                 wrapped_testcase_suite.last_test_temp_dir)
                if wrapped_testcase_suite.last_test_temp_dir and \
                        wrapped_testcase_suite.last_test_vpp_binary:
                    core_path = "%s/core" % \
                                wrapped_testcase_suite.last_test_temp_dir
                    if os.path.isfile(core_path):
                        wrapped_testcase_suite.logger.error(
                            "Core-file exists in test temporary directory: %s!"
                            % core_path)
                        check_core_path(wrapped_testcase_suite.logger,
                                        core_path)
                        wrapped_testcase_suite.logger.debug(
                            "Running `file %s':" % core_path)
                        try:
                            info = check_output(["file", core_path])
                            wrapped_testcase_suite.logger.debug(info)
                        except CalledProcessError as e:
                            wrapped_testcase_suite.logger.error(
                                "Could not run `file' utility on core-file, "
                                "rc=%s" % e.returncode)
                            pass
                        if debug_core:
                            spawn_gdb(
                                wrapped_testcase_suite.last_test_vpp_binary,
                                core_path, wrapped_testcase_suite.logger)
                wrapped_testcase_suite.child.terminate()
                try:
                    # terminating the child process tends to leave orphan
                    # VPP process around
                    os.kill(wrapped_testcase_suite.vpp_pid, signal.SIGTERM)
                except OSError:
                    # already dead
                    pass
                results.append((wrapped_testcase_suite.testcase_suite,
                                wrapped_testcase_suite.partial_result))
                finished_testcase_suites.add(wrapped_testcase_suite)

        for finished_testcase in finished_testcase_suites:
            finished_testcase.child.join()
            finished_testcase.close_pipes()
            wrapped_testcase_suites.remove(finished_testcase)
            finished_unread_testcases.add(finished_testcase)
            finished_testcase.stdouterr_queue.put(None)
            if len(testcases) > 0:
                new_testcase = TestCaseWrapper(testcases.pop(0), manager)
                wrapped_testcase_suites.add(new_testcase)
                unread_testcases.add(new_testcase)

    read_from_testcases.clear()
    stdouterr_thread.join(test_timeout)
    manager.shutdown()
    return results


class SplitToSuitesCallback:
    def __init__(self, filter_callback):
        self.suites = {}
        self.suite_name = 'default'
        self.filter_callback = filter_callback
        self.filtered = unittest.TestSuite()

    def __call__(self, file_name, cls, method):
        test_method = cls(method)
        if self.filter_callback(file_name, cls.__name__, method):
            self.suite_name = file_name + cls.__name__
            if self.suite_name not in self.suites:
                self.suites[self.suite_name] = unittest.TestSuite()
            self.suites[self.suite_name].addTest(test_method)

        else:
            self.filtered.addTest(test_method)


test_option = "TEST"


def parse_test_option():
    f = os.getenv(test_option, None)
    filter_file_name = None
    filter_class_name = None
    filter_func_name = None
    if f:
        if '.' in f:
            parts = f.split('.')
            if len(parts) > 3:
                raise Exception("Unrecognized %s option: %s" %
                                (test_option, f))
            if len(parts) > 2:
                if parts[2] not in ('*', ''):
                    filter_func_name = parts[2]
            if parts[1] not in ('*', ''):
                filter_class_name = parts[1]
            if parts[0] not in ('*', ''):
                if parts[0].startswith('test_'):
                    filter_file_name = parts[0]
                else:
                    filter_file_name = 'test_%s' % parts[0]
        else:
            if f.startswith('test_'):
                filter_file_name = f
            else:
                filter_file_name = 'test_%s' % f
    if filter_file_name:
        filter_file_name = '%s.py' % filter_file_name
    return filter_file_name, filter_class_name, filter_func_name


def filter_tests(tests, filter_cb):
    result = unittest.suite.TestSuite()
    for t in tests:
        if isinstance(t, unittest.suite.TestSuite):
            # this is a bunch of tests, recursively filter...
            x = filter_tests(t, filter_cb)
            if x.countTestCases() > 0:
                result.addTest(x)
        elif isinstance(t, unittest.TestCase):
            # this is a single test
            parts = t.id().split('.')
            # t.id() for common cases like this:
            # test_classifier.TestClassifier.test_acl_ip
            # apply filtering only if it is so
            if len(parts) == 3:
                if not filter_cb(parts[0], parts[1], parts[2]):
                    continue
            result.addTest(t)
        else:
            # unexpected object, don't touch it
            result.addTest(t)
    return result


class FilterByTestOption:
    def __init__(self, filter_file_name, filter_class_name, filter_func_name):
        self.filter_file_name = filter_file_name
        self.filter_class_name = filter_class_name
        self.filter_func_name = filter_func_name

    def __call__(self, file_name, class_name, func_name):
        if self.filter_file_name and file_name != self.filter_file_name:
            return False
        if self.filter_class_name and class_name != self.filter_class_name:
            return False
        if self.filter_func_name and func_name != self.filter_func_name:
            return False
        return True


class FilterByClassList:
    def __init__(self, class_list):
        self.class_list = class_list

    def __call__(self, file_name, class_name, func_name):
        return class_name in self.class_list


def suite_from_failed(suite, failed):
    filter_cb = FilterByClassList(failed)
    suite = filter_tests(suite, filter_cb)
    return suite


class NonPassedResults(dict):
    def __init__(self):
        super(NonPassedResults, self).__init__()
        self.all_testcases = 0
        self.results_per_suite = {}
        self.failures_id = 'failures'
        self.errors_id = 'errors'
        self.crashes_id = 'crashes'
        self.skipped_id = 'skipped'
        self.expectedFailures_id = 'expectedFailures'
        self.unexpectedSuccesses_id = 'unexpectedSuccesses'
        self.rerun = []
        self.passed = 0
        self[self.failures_id] = 0
        self[self.errors_id] = 0
        self[self.skipped_id] = 0
        self[self.expectedFailures_id] = 0
        self[self.unexpectedSuccesses_id] = 0

    def _add_result(self, test, result_id):
        if isinstance(test, VppTestCase):
            parts = test.id().split('.')
            if len(parts) == 3:
                tc_class = get_testcase_doc_name(test)
                if tc_class not in self.results_per_suite:
                    # failed, errored, skipped, expectedly failed,
                    # unexpectedly passed
                    self.results_per_suite[tc_class] = \
                        {self.failures_id: [],
                         self.errors_id: [],
                         self.skipped_id: [],
                         self.expectedFailures_id: [],
                         self.unexpectedSuccesses_id: []}
                self.results_per_suite[tc_class][result_id].append(test)
                return True
        return False

    def add_results(self, testcases, testcase_result_id):
        for failed_testcase, _ in testcases:
            if self._add_result(failed_testcase, testcase_result_id):
                self[testcase_result_id] += 1

    def add_result(self, testcase_suite, result):
        retval = 0
        self.all_testcases += result.testsRun
        self.passed += result.passed
        if result:
            # suite finished properly
            if not result.wasSuccessful():
                retval = 1

            self.add_results(result.failures, self.failures_id)
            self.add_results(result.errors, self.errors_id)
            self.add_results(result.skipped, self.skipped_id)
            self.add_results(result.expectedFailures,
                             self.expectedFailures_id)
            self.add_results(result.unexpectedSuccesses,
                             self.unexpectedSuccesses_id)

        if retval != 0:
            if concurrent_tests == 1:
                if result:
                    rerun_classes = {x[0].__class__.__name__ for
                                     x in result.errors}
                    rerun_classes.update({x[0].__class__.__name__ for
                                          x in result.failures})
                    self.rerun.append(suite_from_failed(testcase_suite,
                                                        rerun_classes))
                else:
                    self.rerun.append(testcase_suite)
            else:
                self.rerun.append(testcase_suite)

        return retval

    def print_results(self):
        print('')
        print(double_line_delim)
        print('TEST RESULTS:')
        print('        Executed tests: {}'.format(self.all_testcases))
        print('          Passed tests: {}'.format(
            colorize(str(self.passed), GREEN)))
        if self[self.failures_id] > 0:
            print('              Failures: {}'.format(
                colorize(str(self[self.failures_id]), RED)))
        if self[self.errors_id] > 0:
            print('                Errors: {}'.format(
                colorize(str(self[self.errors_id]), RED)))
        if self[self.skipped_id] > 0:
            print('         Skipped tests: {}'.format(
                colorize(str(self[self.skipped_id]), YELLOW)))
        if self[self.expectedFailures_id] > 0:
            print('     Expected failures: {}'.format(
                colorize(str(self[self.expectedFailures_id]), GREEN)))
        if self[self.unexpectedSuccesses_id] > 0:
            print('  Unexpected successes: {}'.format(
                colorize(str(self[self.unexpectedSuccesses_id]), YELLOW)))

        if self.all_failed > 0:
            print('FAILED TESTS:')
            for testcase_class, suite_results in \
                    self.results_per_suite.items():
                failed_testcases = suite_results[
                    self.failures_id]
                errored_testcases = suite_results[
                    self.errors_id]
                if len(failed_testcases) or len(errored_testcases):
                    print('  Testcase name: {}'.format(
                        colorize(testcase_class, RED)))
                    for failed_test in failed_testcases:
                        print('     FAILED: {}'.format(
                            colorize(get_test_description(
                                descriptions, failed_test), RED)))
                    for failed_test in errored_testcases:
                        print('    ERRORED: {}'.format(
                            colorize(get_test_description(
                                descriptions, failed_test), RED)))

        print(double_line_delim)
        print('')

    @property
    def all_failed(self):
        return self[self.failures_id] + self[self.errors_id]


def parse_results(results):
    """
    Prints the number of executed, passed, failed, errored, skipped,
    expectedly failed and unexpectedly passed tests and details about
    failed, errored, expectedly failed and unexpectedly passed tests.

    Also returns any suites where any test failed.

    :param results:
    :return:
    """

    results_per_suite = NonPassedResults()
    crashed = False
    failed = False
    for testcase_suite, result in results:
        result_code = results_per_suite.add_result(testcase_suite, result)
        if result_code == 1:
            failed = True
        elif result_code == -1:
            crashed = True

    results_per_suite.print_results()

    if crashed:
        return_code = -1
    elif failed:
        return_code = 1
    else:
        return_code = 0
    return return_code, results_per_suite.rerun


def parse_digit_env(env_var, default):
    value = os.getenv(env_var, default)
    if value != default:
        if value.isdigit():
            value = int(value)
        else:
            print('WARNING: unsupported value "%s" for env var "%s",'
                  'defaulting to %s' % (value, env_var, default))
            value = default
    return value


if __name__ == '__main__':

    verbose = parse_digit_env("V", 0)

    test_timeout = parse_digit_env("TIMEOUT", 600)  # default = 10 minutes

    retries = parse_digit_env("RETRIES", 0)

    debug = os.getenv("DEBUG", "n").lower() in ["gdb", "gdbserver"]

    step = os.getenv("STEP", "n").lower() in ("y", "yes", "1")

    force_foreground = \
        os.getenv("FORCE_FOREGROUND", "n").lower() in ("y", "yes", "1")

    run_interactive = debug or step or force_foreground

    test_jobs = os.getenv("TEST_JOBS", "1").lower()  # default = 1 process
    if test_jobs == 'auto':
        if run_interactive:
            concurrent_tests = 1
            print('Interactive mode required, running on one core')
        else:
            shm_free = psutil.disk_usage('/dev/shm').free
            shm_max_processes = 1
            if shm_free < min_req_shm:
                raise Exception('Not enough free space in /dev/shm. Required '
                                'free space is at least %sM.'
                                % (min_req_shm >> 20))
            else:
                extra_shm = shm_free - min_req_shm
                shm_max_processes += extra_shm / shm_per_process
            concurrent_tests = max(cpu_count(), shm_max_processes)
            print('Found enough resources to run tests with %s cores'
                  % concurrent_tests)
    elif test_jobs.isdigit():
        concurrent_tests = int(test_jobs)
    else:
        concurrent_tests = 1

    if run_interactive and concurrent_tests > 1:
        raise NotImplementedError(
            'Running tests interactively (DEBUG, STEP or FORCE_FOREGROUND is '
            'set) in parallel (TEST_JOBS is more than 1) is not '
            'supported')

    parser = argparse.ArgumentParser(description="VPP unit tests")
    parser.add_argument("-f", "--failfast", action='store_true',
                        help="fast failure flag")
    parser.add_argument("-d", "--dir", action='append', type=str,
                        help="directory containing test files "
                             "(may be specified multiple times)")
    args = parser.parse_args()
    failfast = args.failfast
    descriptions = True

    print("Running tests using custom test runner")  # debug message
    filter_file, filter_class, filter_func = parse_test_option()

    print("Active filters: file=%s, class=%s, function=%s" % (
        filter_file, filter_class, filter_func))

    filter_cb = FilterByTestOption(filter_file, filter_class, filter_func)

    cb = SplitToSuitesCallback(filter_cb)
    for d in args.dir:
        print("Adding tests from directory tree %s" % d)
        discover_tests(d, cb)

    # suites are not hashable, need to use list
    suites = []
    tests_amount = 0
    for testcase_suite in cb.suites.values():
        tests_amount += testcase_suite.countTestCases()
        suites.append(testcase_suite)

    if concurrent_tests == 1:
        new_suite = unittest.TestSuite()
        for suite in suites:
            new_suite.addTest(suite)

        suites = [new_suite]

    print("%s out of %s tests match specified filters" % (
        tests_amount, tests_amount + cb.filtered.countTestCases()))

    if not running_extended_tests():
        print("Not running extended tests (some tests will be skipped)")

    attempts = retries + 1
    if attempts > 1:
        print("Perform %s attempts to pass the suite..." % attempts)

    if run_interactive:
        # don't fork if requiring interactive terminal
        sys.exit(not VppTestRunner(
            verbosity=verbose, failfast=failfast)
                 .run(suites[0]).wasSuccessful())
    else:
        exit_code = 0
        while len(suites) > 0 and attempts > 0:
            tests_amount = sum([x.countTestCases() for x in suites])
            results = run_forked(suites)
            exit_code, suites = parse_results(results)
            attempts -= 1
            if exit_code == 0:
                print('Test run was successful')
            else:
                print('%s attempt(s) left.' % attempts)
        sys.exit(exit_code)