#!/usr/bin/env python from __future__ import print_function import gc import sys import os import select import unittest import tempfile import time import resource import faulthandler 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 from scapy.packet import Raw from hook import StepHook, PollHook from vpp_pg_interface import VppPGInterface from vpp_sub_interface import VppSubInterface from vpp_lo_interface import VppLoInterface from vpp_papi_provider import VppPapiProvider from log import * from vpp_object import VppObjectRegistry 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 import subprocess32 as subprocess else: import subprocess """ Test framework module. The module provides a set of tools for constructing and running tests and representing the results. """ class _PacketInfo(object): """Private class to create packet info object. Help process information about the next packet. Set variables to default values. """ #: Store the index of the packet. index = -1 #: Store the index of the source packet generator interface of the packet. src = -1 #: Store the index of the destination packet generator interface #: of the packet. dst = -1 #: Store expected ip version ip = -1 #: Store expected upper protocol proto = -1 #: Store the copy of the former packet. data = None def __eq__(self, other): index = self.index == other.index src = self.src == other.src dst = self.dst == other.dst data = self.data == other.data return index and src and dst and data def pump_output(testclass): """ pump output from vpp stdout/stderr to proper queues """ while not testclass.pump_thread_stop_flag.wait(0): readable = select.select([testclass.vpp.stdout.fileno(), testclass.vpp.stderr.fileno(), testclass.pump_thread_wakeup_pipe[0]], [], [])[0] if testclass.vpp.stdout.fileno() in readable: read = os.read(testclass.vpp.stdout.fileno(), 1024) testclass.vpp_stdout_deque.append(read) if not testclass.cache_vpp_output: for line in read.splitlines(): testclass.logger.debug("VPP STDOUT: %s" % line) if testclass.vpp.stderr.fileno() in readable: read = os.read(testclass.vpp.stderr.fileno(), 1024) testclass.vpp_stderr_deque.append(read) if not testclass.cache_vpp_output: for line in read.splitlines(): testclass.logger.debug("VPP STDERR: %s" % line) # ignoring the dummy pipe here intentionally - the flag will take care # of properly terminating the loop def running_extended_tests(): try: s = os.getenv("EXTENDED_TESTS") return True if s.lower() in ("y", "yes", "1") else False except: return False return False def running_on_centos(): try: os_id = os.getenv("OS_ID") return True if "centos" in os_id.lower() else False except: return False return False class KeepAliveReporter(object): """ Singleton object which reports test start to parent process """ _shared_state = {} def __init__(self): self.__dict__ = self._shared_state @property def pipe(self): return self._pipe @pipe.setter def pipe(self, pipe): if hasattr(self, '_pipe'): raise Exception("Internal error - pipe should only be set once.") self._pipe = pipe def send_keep_alive(self, test): """ Write current test tmpdir & desc to keep-alive pipe to signal liveness """ if self.pipe is None: # if not running forked.. return if isclass(test): desc = test.__name__ else: desc = test.shortDescription() if not desc: desc = str(test) self.pipe.send((desc, test.vpp_bin, test.tempdir, test.vpp.pid)) class VppTestCase(unittest.TestCase): """This subclass is a base class for VPP test cases that are implemented as classes. It provides methods to create and run test case. """ @property def packet_infos(self): """List of packet infos""" return self._packet_infos @classmethod def get_packet_count_for_if_idx(cls, dst_if_index): """Get the number of packet info for specified destination if index""" if dst_if_index in cls._packet_count_for_dst_if_idx: return cls._packet_count_for_dst_if_idx[dst_if_index] else: return 0 @classmethod def instance(cls): """Return the instance of this testcase""" return cls.test_instance @classmethod def set_debug_flags(cls, d): cls.debug_core = False cls.debug_gdb = False cls.debug_gdbserver = False if d is None: return dl = d.lower() if dl == "core": cls.debug_core = True elif dl == "gdb": cls.debug_gdb = True elif dl == "gdbserver": cls.debug_gdbserver = True else: raise Exception("Unrecognized DEBUG option: '%s'" % d) @classmethod def setUpConstants(cls): """ Set-up the test case class based on environment variables """ try: s = os.getenv("STEP") cls.step = True if s.lower() in ("y", "yes", "1") else False except: cls.step = False try: d = os.getenv("DEBUG") except: d = None try: c = os.getenv("CACHE_OUTPUT", "1") cls.cache_vpp_output = \ True if c.lower() in ("y", "yes", "1") else False except: cls.cache_vpp_output = True cls.set_debug_flags(d) cls.vpp_bin = os.getenv('VPP_TEST_BIN', "vpp") cls.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: if cls.extern_plugin_path is not None: plugin_path = "%s:%s" % ( cls.plugin_path, cls.extern_plugin_path) else: plugin_path = cls.plugin_path elif cls.extern_plugin_path is not None: plugin_path = cls.extern_plugin_path debug_cli = "" if cls.step or cls.debug_gdb or cls.debug_gdbserver: debug_cli = "cli-listen localhost:5002" coredump_size = None try: size = os.getenv("COREDUMP_SIZE") if size is not None: coredump_size = "coredump-size %s" % size except: pass if coredump_size is None: coredump_size = "coredump-size unlimited" cls.vpp_cmdline = [cls.vpp_bin, "unix", "{", "nodaemon", debug_cli, "full-coredump", coredump_size, "}", "api-trace", "{", "on", "}", "api-segment", "{", "prefix", cls.shm_prefix, "}", "plugins", "{", "plugin", "dpdk_plugin.so", "{", "disable", "}", "}"] if plugin_path is not None: cls.vpp_cmdline.extend(["plugin_path", plugin_path]) cls.logger.info("vpp_cmdline: %s" % cls.vpp_cmdline) @classmethod def wait_for_enter(cls): if cls.debug_gdbserver: print(double_line_delim) print("Spawned GDB server with PID: %d" % cls.vpp.pid) elif cls.debug_gdb: print(double_line_delim) print("Spawned VPP with PID: %d" % cls.vpp.pid) else: cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid) return 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("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("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...") @classmethod def run_vpp(cls): cmdline = cls.vpp_cmdline if cls.debug_gdbserver: gdbserver = '/usr/bin/gdbserver' if not os.path.isfile(gdbserver) or \ not os.access(gdbserver, os.X_OK): raise Exception("gdbserver binary '%s' does not exist or is " "not executable" % gdbserver) cmdline = [gdbserver, 'localhost:7777'] + cls.vpp_cmdline cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline)) try: cls.vpp = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) except Exception as e: cls.logger.critical("Couldn't start vpp: %s" % e) raise cls.wait_for_enter() @classmethod def setUpClass(cls): """ Perform class setup before running the testcase Remove shared memory files, start vpp and connect the vpp-api """ gc.collect() # run garbage collection first cls.logger = getLogger(cls.__name__) cls.tempdir = tempfile.mkdtemp( prefix='vpp-unittest-%s-' % cls.__name__) 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.shm_prefix = cls.tempdir.split("/")[-1] os.chdir(cls.tempdir) cls.logger.
/*
 * Copyright (c) 2019 Cisco and/or its affiliates.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <vnet/fib/fib_entry_track.h>
#include <vnet/fib/fib_table.h>
#include <vnet/fib/fib_entry_delegate.h>
#include <vnet/fib/fib_walk.h>

static fib_entry_delegate_t *
fib_entry_track_delegate_add (u32 fib_index,
                              const fib_prefix_t *prefix)
{
    fib_entry_delegate_t *fed;
    fib_node_index_t fei;

    fei = fib_table_entry_special_add(fib_index,
                                      prefix,
                                      FIB_SOURCE_RR,
                                      FIB_ENTRY_FLAG_NONE);

    fed = fib_entry_delegate_find_or_add(fib_entry_get(fei),
                                         FIB_ENTRY_DELEGATE_TRACK);

    fib_node_init(&fed->fd_track.fedt_node,
                  FIB_NODE_TYPE_ENTRY_TRACK);

    fed->fd_entry_index = fei;
    fed->fd_track.fedt_sibling =
        fib_entry_child_add(fei,
                            FIB_NODE_TYPE_ENTRY_TRACK,
                            fib_entry_delegate_get_index(fed));

    return (fed);
}

fib_node_index_t
fib_entry_track (u32 fib_index,
                 const fib_prefix_t *prefix,
                 fib_node_type_t child_type,
                 index_t child_index,
                 u32 *sibling)
{
    fib_entry_delegate_t *fed;
    fib_node_index_t fei;

    fei = fib_table_lookup_exact_match(fib_index, prefix);

    if (INDEX_INVALID == fei ||
        NULL == (fed = fib_entry_delegate_find(fib_entry_get(fei),
                                               FIB_ENTRY_DELEGATE_TRACK)))
    {
        fed = fib_entry_track_delegate_add(fib_index, prefix);
    }

    /*
     * add this child to the entry's delegate
     */
    *sibling = fib_node_child_add(FIB_NODE_TYPE_ENTRY_TRACK,
                                  fib_entry_delegate_get_index(fed),
                                  child_type,
                                  child_index);

    return (fed->fd_entry_index);
}

void
fib_entry_untrack (fib_node_index_t fei,
                   u32 sibling)
{
    fib_entry_delegate_t *fed;

    fed = fib_entry_delegate_find(fib_entry_get(fei),
                                  FIB_ENTRY_DELEGATE_TRACK);

    if (NULL != fed)
    {
        fib_node_child_remove(FIB_NODE_TYPE_ENTRY_TRACK,
                              fib_entry_delegate_get_index(fed),
                              sibling);
        /* if this is the last child the delegate will be removed. */
    }
    /* else untracked */
}

static fib_node_t *
fib_entry_track_get_node (fib_node_index_t index)
{
    fib_entry_delegate_t *fed;

    fed = fib_entry_delegate_get(index);
    return (&fed->fd_track.fedt_node);
}

static fib_entry_delegate_t*
fib_entry_delegate_from_fib_node (fib_node_t *node)
{
    ASSERT(FIB_NODE_TYPE_ENTRY_TRACK == node->fn_type);
    return ((fib_entry_delegate_t *) (((char *) node) -
                                      STRUCT_OFFSET_OF (fib_entry_delegate_t,
                                                        fd_track.fedt_node)));
}

static void
fib_entry_track_last_lock_gone (fib_node_t *node)
{
    fib_entry_delegate_t *fed;
    fib_node_index_t fei;
    u32 sibling;

    fed = fib_entry_delegate_from_fib_node(node);
    fei = fed->fd_entry_index;
    sibling = fed->fd_track.fedt_sibling;

    /*
     * the tracker has no more children so it can be removed,
     * and the FIB entry unsourced.
     * remove the delegate first, then unlock the fib entry,
     * since the delegate may be holding the last lock
     */
    fib_entry_delegate_remove(fib_entry_get(fei),
                              FIB_ENTRY_DELEGATE_TRACK);
    /* having removed the deletegate the fed object is now toast */
    fib_entry_child_remove(fei, sibling);

    fib_table_entry_delete_index(fei, FIB_SOURCE_RR);
}

static fib_node_back_walk_rc_t
fib_entry_track_back_walk_notify (fib_node_t *node,
                                  fib_node_back_walk_ctx_t *ctx)
{
    fib_entry_delegate_t *fed;

    fed = fib_entry_delegate_from_fib_node(node);

    /*
     * propagate the walk to the delgate's children
     */
    
    fib_walk_sync(FIB_NODE_TYPE_ENTRY_TRACK,
                  fib_entry_delegate_get_index(fed),
                  ctx);

    return (FIB_NODE_BACK_WALK_CONTINUE);
}

static void
fib_entry_track_show_memory (void)
{
}

/*
 * The FIB entry tracker's graph node virtual function table
 */
static const fib_node_vft_t fib_entry_track_vft = {
    .fnv_get = fib_entry_track_get_node,
    .fnv_last_lock = fib_entry_track_last_lock_gone,
    .fnv_back_walk = fib_entry_track_back_walk_notify,
    .fnv_mem_show = fib_entry_track_show_memory,
};

void
fib_entry_track_module_init (void)
{
    fib_node_register_type(FIB_NODE_TYPE_ENTRY_TRACK, &fib_entry_track_vft);
}
est) unittest.TestResult.startTest(self, test) if self.verbosity > 0: self.stream.writeln( "Starting " + self.getDescription(test) + " ...") self.stream.writeln(single_line_delim) def stopTest(self, test): """ Stop a test :param test: """ unittest.TestResult.stopTest(self, test) if self.verbosity > 0: self.stream.writeln(single_line_delim) self.stream.writeln("%-73s%s" % (self.getDescription(test), self.result_string)) self.stream.writeln(single_line_delim) else: self.stream.writeln("%-73s%s" % (self.getDescription(test), self.result_string)) def printErrors(self): """ Print errors from running the test case """ self.stream.writeln() self.printErrorList('ERROR', self.errors) self.printErrorList('FAIL', self.failures) def printErrorList(self, flavour, errors): """ Print error list to the output stream together with error type and test case description. :param flavour: error type :param errors: iterable errors """ for test, err in errors: self.stream.writeln(double_line_delim) self.stream.writeln("%s: %s" % (flavour, self.getDescription(test))) self.stream.writeln(single_line_delim) self.stream.writeln("%s" % err) class Filter_by_test_option: 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 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""" return VppTestResult def __init__(self, keep_alive_pipe=None, failed_pipe=None, stream=sys.stderr, descriptions=True, verbosity=1, failfast=False, buffer=False, resultclass=None): # 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) reporter = KeepAliveReporter() reporter.pipe = keep_alive_pipe # this is super-ugly, but very simple to implement and works as long # as we run only one test at the same time VppTestResult.test_framework_failed_pipe = failed_pipe test_option = "TEST" def parse_test_option(self): try: f = os.getenv(self.test_option) except: f = 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" % (self.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 return filter_file_name, filter_class_name, filter_func_name @staticmethod 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 def run(self, test): """ Run the tests :param test: """ faulthandler.enable() # emit stack trace to stderr if killed by signal print("Running tests using custom test runner") # debug message filter_file, filter_class, filter_func = self.parse_test_option() print("Active filters: file=%s, class=%s, function=%s" % ( filter_file, filter_class, filter_func)) filter_cb = Filter_by_test_option( filter_file, filter_class, filter_func) filtered = self.filter_tests(test, filter_cb) print("%s out of %s tests match specified filters" % ( filtered.countTestCases(), test.countTestCases())) if not running_extended_tests(): print("Not running extended tests (some tests will be skipped)") return super(VppTestRunner, self).run(filtered) class Worker(Thread): def __init__(self, args, logger): self.logger = logger self.args = args self.result = None super(Worker, self).__init__() def run(self): executable = self.args[0] self.logger.debug("Running executable w/args `%s'" % self.args) env = os.environ.copy() env["CK_LOG_FILE_NAME"] = "-" self.process = subprocess.Popen( self.args, shell=False, env=env, preexec_fn=os.setpgrp, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = self.process.communicate() self.logger.debug("Finished running `%s'" % executable) self.logger.info("Return code is `%s'" % self.process.returncode) self.logger.info(single_line_delim) self.logger.info("Executable `%s' wrote to stdout:" % executable) self.logger.info(single_line_delim) self.logger.info(out) self.logger.info(single_line_delim) self.logger.info("Executable `%s' wrote to stderr:" % executable) self.logger.info(single_line_delim) self.logger.error(err) self.logger.info(single_line_delim) self.result = self.process.returncode