tests: support attaching to existing vpp
[vpp.git] / test / framework.py
index 4e0949b..67ac495 100644 (file)
@@ -281,6 +281,17 @@ tag_run_solo = create_tag_decorator(TestCaseTag.RUN_SOLO)
 tag_fixme_vpp_workers = create_tag_decorator(TestCaseTag.FIXME_VPP_WORKERS)
 
 
+class DummyVpp:
+    returncode = None
+    pid = 0xcafebafe
+
+    def poll(self):
+        pass
+
+    def terminate(self):
+        pass
+
+
 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.
@@ -330,6 +341,7 @@ class VppTestCase(unittest.TestCase):
         cls.debug_gdb = False
         cls.debug_gdbserver = False
         cls.debug_all = False
+        cls.debug_attach = False
         if d is None:
             return
         dl = d.lower()
@@ -339,6 +351,8 @@ class VppTestCase(unittest.TestCase):
             cls.debug_gdb = True
         elif dl == "gdbserver" or dl == "gdbserver-all":
             cls.debug_gdbserver = True
+        elif dl == "attach":
+            cls.debug_attach = True
         else:
             raise Exception("Unrecognized DEBUG option: '%s'" % d)
         if dl == "gdb-all" or dl == "gdbserver-all":
@@ -377,11 +391,9 @@ class VppTestCase(unittest.TestCase):
     def setUpConstants(cls):
         """ Set-up the test case class based on environment variables """
         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')
@@ -406,11 +418,18 @@ 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 = os.getenv("VPP_WORKER_CONFIG", "")
-            if cls.worker_config != "":
-                if cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
-                    cls.worker_config = ""
+        if not hasattr(cls, "vpp_worker_count"):
+            cls.vpp_worker_count = 0
+            worker_config = os.getenv("VPP_WORKER_CONFIG", "")
+            if worker_config:
+                elems = worker_config.split(" ")
+                if elems[0] != "workers" or len(elems) != 2:
+                    raise ValueError("Wrong VPP_WORKER_CONFIG == '%s' value." %
+                                     worker_config)
+                cls.vpp_worker_count = int(elems[1])
+                if cls.vpp_worker_count > 0 and\
+                        cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
+                    cls.vpp_worker_count = 0
 
         default_variant = os.getenv("VARIANT")
         if default_variant is not None:
@@ -422,25 +441,27 @@ class VppTestCase(unittest.TestCase):
         if api_fuzzing is None:
             api_fuzzing = 'off'
 
-        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),
-                           cls.worker_config, "}",
-                           "physmem", "{", "max-size", "32m", "}",
-                           "statseg", "{", "socket-name", cls.stats_sock, "}",
-                           "socksvr", "{", "socket-name", cls.api_sock, "}",
-                           "node { ", default_variant, "}",
-                           "api-fuzz {", api_fuzzing, "}",
-                           "plugins",
-                           "{", "plugin", "dpdk_plugin.so", "{", "disable",
-                           "}", "plugin", "rdma_plugin.so", "{", "disable",
-                           "}", "plugin", "lisp_unittest_plugin.so", "{",
-                           "enable",
-                           "}", "plugin", "unittest_plugin.so", "{", "enable",
-                           "}"] + cls.extra_vpp_plugin_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.get_api_segment_prefix(), "}",
+            "cpu", "{", "main-core", str(cpu_core_number), ]
+        if cls.vpp_worker_count:
+            cls.vpp_cmdline.extend(["workers", str(cls.vpp_worker_count)])
+        cls.vpp_cmdline.extend([
+            "}",
+            "physmem", "{", "max-size", "32m", "}",
+            "statseg", "{", "socket-name", cls.get_stats_sock_path(), "}",
+            "socksvr", "{", "socket-name", cls.get_api_sock_path(), "}",
+            "node { ", default_variant, "}",
+            "api-fuzz {", api_fuzzing, "}",
+            "plugins", "{", "plugin", "dpdk_plugin.so", "{", "disable", "}",
+            "plugin", "rdma_plugin.so", "{", "disable", "}",
+            "plugin", "lisp_unittest_plugin.so", "{", "enable", "}",
+            "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)
@@ -449,8 +470,9 @@ class VppTestCase(unittest.TestCase):
         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))
+        if not cls.debug_attach:
+            cls.logger.info("vpp_cmdline args: %s" % cls.vpp_cmdline)
+            cls.logger.info("vpp_cmdline: %s" % " ".join(cls.vpp_cmdline))
 
     @classmethod
     def wait_for_enter(cls):
@@ -481,6 +503,10 @@ class VppTestCase(unittest.TestCase):
         print(single_line_delim)
         input("Press ENTER to continue running the testcase...")
 
+    @classmethod
+    def attach_vpp(cls):
+        cls.vpp = DummyVpp()
+
     @classmethod
     def run_vpp(cls):
         cmdline = cls.vpp_cmdline
@@ -537,6 +563,26 @@ class VppTestCase(unittest.TestCase):
                 cls.logger.error("Coredump complete: %s, size %d",
                                  corefile, curr_size)
 
+    @classmethod
+    def get_stats_sock_path(cls):
+        return "%s/stats.sock" % cls.tempdir
+
+    @classmethod
+    def get_api_sock_path(cls):
+        return "%s/api.sock" % cls.tempdir
+
+    @classmethod
+    def get_api_segment_prefix(cls):
+        return os.path.basename(cls.tempdir)  # Only used for VAPI
+
+    @classmethod
+    def get_tempdir(cls):
+        if cls.debug_attach:
+            return os.getenv("VPP_IN_GDB_TMP_DIR",
+                             "/tmp/unittest-attach-gdb")
+        else:
+            return tempfile.mkdtemp(prefix='vpp-unittest-%s-' % cls.__name__)
+
     @classmethod
     def setUpClass(cls):
         """
@@ -544,34 +590,30 @@ class VppTestCase(unittest.TestCase):
         Remove shared memory files, start vpp and connect the vpp-api
         """
         super(VppTestCase, cls).setUpClass()
-        gc.collect()  # run garbage collection first
         cls.logger = get_logger(cls.__name__)
         seed = os.environ["RND_SEED"]
         random.seed(seed)
         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
+        d = os.getenv("DEBUG", None)
+        cls.set_debug_flags(d)
+        cls.tempdir = cls.get_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)  # Only used for VAPI
+        cls.logger.debug("--- setUpClass() for %s called ---" % cls.__name__)
         os.chdir(cls.tempdir)
         cls.logger.info("Temporary dir is %s, api socket is %s",
-                        cls.tempdir, cls.api_sock)
-        cls.logger.debug("Random seed is %s" % seed)
+                        cls.tempdir, cls.get_api_sock_path())
+        cls.logger.debug("Random seed is %s", seed)
         cls.setUpConstants()
         cls.reset_packet_infos()
-        cls._captures = []
+        cls._pcaps = []
+        cls._old_pcaps = []
         cls.verbose = 0
         cls.vpp_dead = False
         cls.registry = VppObjectRegistry()
@@ -580,18 +622,22 @@ class VppTestCase(unittest.TestCase):
         # need to catch exceptions here because if we raise, then the cleanup
         # doesn't get called and we might end with a zombie vpp
         try:
-            cls.run_vpp()
+            if cls.debug_attach:
+                cls.attach_vpp()
+            else:
+                cls.run_vpp()
             cls.reporter.send_keep_alive(cls, 'setUpClass')
             VppTestResult.current_test_case_info = TestCaseInfo(
                 cls.logger, cls.tempdir, cls.vpp.pid, cls.vpp_bin)
             cls.vpp_stdout_deque = deque()
             cls.vpp_stderr_deque = deque()
-            cls.pump_thread_stop_flag = Event()
-            cls.pump_thread_wakeup_pipe = os.pipe()
-            cls.pump_thread = Thread(target=pump_output, args=(cls,))
-            cls.pump_thread.daemon = True
-            cls.pump_thread.start()
-            if cls.debug_gdb or cls.debug_gdbserver:
+            if not cls.debug_attach:
+                cls.pump_thread_stop_flag = Event()
+                cls.pump_thread_wakeup_pipe = os.pipe()
+                cls.pump_thread = Thread(target=pump_output, args=(cls,))
+                cls.pump_thread.daemon = True
+                cls.pump_thread.start()
+            if cls.debug_gdb or cls.debug_gdbserver or cls.debug_attach:
                 cls.vapi_response_timeout = 0
             cls.vapi = VppPapiProvider(cls.__name__, cls,
                                        cls.vapi_response_timeout)
@@ -600,7 +646,7 @@ class VppTestCase(unittest.TestCase):
             else:
                 hook = hookmodule.PollHook(cls)
             cls.vapi.register_hook(hook)
-            cls.statistics = VPPStats(socketname=cls.stats_sock)
+            cls.statistics = VPPStats(socketname=cls.get_stats_sock_path())
             try:
                 hook.poll_vpp()
             except VppDiedError:
@@ -620,6 +666,10 @@ class VppTestCase(unittest.TestCase):
                                    "VPP-API connection failed, did you forget "
                                    "to 'continue' VPP from within gdb?", RED))
                 raise e
+            if cls.debug_attach:
+                last_line = cls.vapi.cli("show thread").split("\n")[-2]
+                cls.vpp_worker_count = int(last_line.split(" ")[0])
+                print("Detected VPP with %s workers." % cls.vpp_worker_count)
         except vpp_papi.VPPRuntimeError as e:
             cls.logger.debug("%s" % e)
             cls.quit()
@@ -674,7 +724,7 @@ class VppTestCase(unittest.TestCase):
                                  cls.__name__)
                 del cls.vapi
             cls.vpp.poll()
-            if cls.vpp.returncode is None:
+            if not cls.debug_attach and cls.vpp.returncode is None:
                 cls.wait_for_coredump()
                 cls.logger.debug("Sending TERM to vpp")
                 cls.vpp.terminate()
@@ -686,8 +736,9 @@ class VppTestCase(unittest.TestCase):
                     outs, errs = cls.vpp.communicate()
             cls.logger.debug("Deleting class vpp attribute on %s",
                              cls.__name__)
-            cls.vpp.stdout.close()
-            cls.vpp.stderr.close()
+            if not cls.debug_attach:
+                cls.vpp.stdout.close()
+                cls.vpp.stderr.close()
             del cls.vpp
 
         if cls.vpp_startup_failed:
@@ -774,7 +825,6 @@ class VppTestCase(unittest.TestCase):
         super(VppTestCase, self).setUp()
         self.reporter.send_keep_alive(self)
         if self.vpp_dead:
-
             raise VppDiedError(rv=None, testcase=self.__class__.__name__,
                                method_name=self._testMethodName)
         self.sleep(.1, "during setUp")
@@ -806,10 +856,10 @@ class VppTestCase(unittest.TestCase):
             i.enable_capture()
 
     @classmethod
-    def register_capture(cls, cap_name):
-        """ Register a capture in the testclass """
+    def register_pcap(cls, intf, worker):
+        """ Register a pcap in the testclass """
         # add to the list of captures with current timestamp
-        cls._captures.append((time.time(), cap_name))
+        cls._pcaps.append((intf, worker))
 
     @classmethod
     def get_vpp_time(cls):
@@ -833,6 +883,10 @@ class VppTestCase(unittest.TestCase):
     @classmethod
     def pg_start(cls, trace=True):
         """ Enable the PG, wait till it is done, then clean up """
+        for (intf, worker) in cls._old_pcaps:
+            intf.rename_old_pcap_file(intf.get_in_path(worker),
+                                      intf.in_history_counter)
+        cls._old_pcaps = []
         if trace:
             cls.vapi.cli("clear trace")
             cls.vapi.cli("trace add pg-input 1000")
@@ -847,9 +901,11 @@ class VppTestCase(unittest.TestCase):
             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 = []
+        for intf, worker in cls._pcaps:
+            cls.vapi.cli('packet-generator delete %s' %
+                         intf.get_cap_name(worker))
+        cls._old_pcaps = cls._pcaps
+        cls._pcaps = []
 
     @classmethod
     def create_pg_interfaces(cls, interfaces, gso=0, gso_size=0):
@@ -1169,7 +1225,7 @@ class VppTestCase(unittest.TestCase):
                           "packet counter `%s'" % counter)
 
     def assert_error_counter_equal(self, counter, expected_value):
-        counter_value = self.statistics.get_err_counter(counter)
+        counter_value = self.statistics[counter].sum()
         self.assert_equal(counter_value, expected_value,
                           "error counter `%s'" % counter)