tests: run tests against a running VPP 64/37064/5
authorNaveen Joy <najoy@cisco.com>
Tue, 30 Aug 2022 20:59:03 +0000 (13:59 -0700)
committerDamjan Marion <dmarion@0xa5.net>
Tue, 20 Sep 2022 13:54:58 +0000 (13:54 +0000)
Usage:
test/run.py -r  -t {test_filter}
Instead of starting a new instance of VPP, when the -r argument
is provided, test is run against a running VPP instance. Optionally,
one can also set the VPP socket directory using the -d
argument. The default location for socket files is
/var/run/user/${uid}/vpp and /var/run/vpp if VPP is started
as root.

Type: improvement

Change-Id: I05e57a067fcb90fb49973f8159fc17925b741f1a
Signed-off-by: Naveen Joy <najoy@cisco.com>
test/config.py
test/framework.py
test/run.py
test/vpp_running.py [new file with mode: 0644]

index b8bbbbc..e735557 100644 (file)
@@ -359,6 +359,29 @@ parser.add_argument(
     help=f"if set, keep all pcap files from a test run (default: {default_keep_pcaps})",
 )
 
+parser.add_argument(
+    "-r",
+    "--use-running-vpp",
+    dest="running_vpp",
+    required=False,
+    action="store_true",
+    default=False,
+    help="Runs tests against a running VPP.",
+)
+
+parser.add_argument(
+    "-d",
+    "--socket-dir",
+    dest="socket_dir",
+    required=False,
+    action="store",
+    default="",
+    help="Relative or absolute path to running VPP's socket directory.\n"
+    "The directory must contain VPP's socket files:api.sock & stats.sock.\n"
+    "Default: /var/run/vpp if VPP is started as the root user, else "
+    "/var/run/user/${uid}/vpp.",
+)
+
 config = parser.parse_args()
 
 ws = config.vpp_ws_dir
index 230b2d5..c85dec5 100644 (file)
@@ -51,6 +51,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
+from vpp_running import use_running
 
 
 logger = logging.getLogger(__name__)
@@ -302,6 +303,7 @@ class CPUInterface(ABC):
         cls.cpus = cpus
 
 
+@use_running
 class VppTestCase(CPUInterface, 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.
@@ -698,7 +700,8 @@ class VppTestCase(CPUInterface, unittest.TestCase):
             )
             cls.vpp_stdout_deque = deque()
             cls.vpp_stderr_deque = deque()
-            if not cls.debug_attach:
+            # Pump thread in a non-debug-attached & not running-vpp
+            if not cls.debug_attach and not hasattr(cls, "running_vpp"):
                 cls.pump_thread_stop_flag = Event()
                 cls.pump_thread_wakeup_pipe = os.pipe()
                 cls.pump_thread = Thread(target=pump_output, args=(cls,))
@@ -775,6 +778,8 @@ class VppTestCase(CPUInterface, unittest.TestCase):
         Disconnect vpp-api, kill vpp and cleanup shared memory files
         """
         cls._debug_quit()
+        if hasattr(cls, "running_vpp"):
+            cls.vpp.quit_vpp()
 
         # first signal that we want to stop the pump thread, then wake it up
         if hasattr(cls, "pump_thread_stop_flag"):
@@ -807,10 +812,16 @@ class VppTestCase(CPUInterface, unittest.TestCase):
                     cls.vpp.kill()
                     outs, errs = cls.vpp.communicate()
             cls.logger.debug("Deleting class vpp attribute on %s", cls.__name__)
-            if not cls.debug_attach:
+            if not cls.debug_attach and not hasattr(cls, "running_vpp"):
                 cls.vpp.stdout.close()
                 cls.vpp.stderr.close()
-            del cls.vpp
+            # If vpp is a dynamic attribute set by the func use_running,
+            # deletion will result in an AttributeError that we can
+            # safetly pass.
+            try:
+                del cls.vpp
+            except AttributeError:
+                pass
 
         if cls.vpp_startup_failed:
             stdout_log = cls.logger.info
index 07b24d5..646354a 100755 (executable)
@@ -21,7 +21,7 @@ import logging
 import os
 from pathlib import Path
 import signal
-from subprocess import Popen, PIPE, STDOUT
+from subprocess import Popen, PIPE, STDOUT, call
 import sys
 import time
 import venv
@@ -31,7 +31,7 @@ import venv
 test_dir = os.path.dirname(os.path.realpath(__file__))
 ws_root = os.path.dirname(test_dir)
 build_root = os.path.join(ws_root, "build-root")
-venv_dir = os.path.join(test_dir, "venv")
+venv_dir = os.path.join(build_root, "test", "venv")
 venv_bin_dir = os.path.join(venv_dir, "bin")
 venv_lib_dir = os.path.join(venv_dir, "lib")
 venv_run_dir = os.path.join(venv_dir, "run")
@@ -215,8 +215,9 @@ def set_environ():
 # Runs a test inside a spawned QEMU VM
 # If a kernel image is not provided, a linux-image-kvm image is
 # downloaded to the test_data_dir
-def vm_test_runner(test_name, kernel_image, test_data_dir, cpu_mask, mem):
+def vm_test_runner(test_name, kernel_image, test_data_dir, cpu_mask, mem, jobs="auto"):
     script = os.path.join(test_dir, "scripts", "run_vpp_in_vm.sh")
+    os.environ["TEST_JOBS"] = str(jobs)
     p = Popen(
         [script, test_name, kernel_image, test_data_dir, cpu_mask, mem],
         stdout=PIPE,
@@ -275,20 +276,53 @@ def set_logging(test_data_dir, test_name):
     logging.basicConfig(filename=filename, level=logging.DEBUG)
 
 
+def run_tests_in_venv(
+    test,
+    jobs,
+    log_dir,
+    socket_dir="",
+    running_vpp=False,
+):
+    """Runs tests in the virtual environment set by venv_dir.
+
+    Arguments:
+    test: Name of the test to run
+    jobs: Maximum concurrent test jobs
+    log_dir: Directory location for storing log files
+    socket_dir: Use running VPP's socket files
+    running_vpp: True if tests are run against a running VPP
+    """
+    script = os.path.join(test_dir, "scripts", "run.sh")
+    args = [
+        f"--venv-dir={venv_dir}",
+        f"--vpp-ws-dir={ws_root}",
+        f"--socket-dir={socket_dir}",
+        f"--filter={test}",
+        f"--jobs={jobs}",
+        f"--log-dir={log_dir}",
+    ]
+    if running_vpp:
+        args = args + [f"--use-running-vpp"]
+    print(f"Running script: {script} " f"{' '.join(args)}")
+    process_args = [script] + args
+    call(process_args)
+
+
 if __name__ == "__main__":
     # Build a Virtual Environment for running tests on host & QEMU
+    # (TODO): Create a single config object by merging the below args with
+    # config.py after gathering dev use-cases.
     parser = argparse.ArgumentParser(
         description="Run VPP Unit Tests", formatter_class=argparse.RawTextHelpFormatter
     )
     parser.add_argument(
         "--vm",
         dest="vm",
-        required=True,
+        required=False,
         action="store_true",
         help="Run Test Inside a QEMU VM",
     )
     parser.add_argument(
-        "-d",
         "--debug",
         dest="debug",
         required=False,
@@ -297,7 +331,6 @@ if __name__ == "__main__":
         help="Run Tests on Debug Build",
     )
     parser.add_argument(
-        "-r",
         "--release",
         dest="release",
         required=False,
@@ -306,12 +339,13 @@ if __name__ == "__main__":
         help="Run Tests on release Build",
     )
     parser.add_argument(
+        "-t",
         "--test",
         dest="test_name",
         required=False,
         action="store",
         default="",
-        help="Tests to Run",
+        help="Test Name or Test filter",
     )
     parser.add_argument(
         "--vm-kernel-image",
@@ -339,7 +373,42 @@ if __name__ == "__main__":
         default="2",
         help="Guest Memory in Gibibytes\n" "E.g. 4 (Default: 2)",
     )
+    parser.add_argument(
+        "--log-dir",
+        action="store",
+        default="/tmp",
+        help="directory where to store directories "
+        "containing log files (default: /tmp)",
+    )
+    parser.add_argument(
+        "--jobs",
+        action="store",
+        default="auto",
+        help="maximum concurrent test jobs",
+    )
+    parser.add_argument(
+        "-r",
+        "--use-running-vpp",
+        dest="running_vpp",
+        required=False,
+        action="store_true",
+        default=False,
+        help="Runs tests against a running VPP.",
+    )
+    parser.add_argument(
+        "-d",
+        "--socket-dir",
+        dest="socket_dir",
+        required=False,
+        action="store",
+        default="",
+        help="Relative or absolute path of running VPP's socket directory "
+        "containing api.sock & stats.sock files.\n"
+        "Default: /var/run/vpp if VPP is started as the root user, else "
+        "/var/run/user/${uid}/vpp.",
+    )
     args = parser.parse_args()
+    vm_tests = False
     # Enable VM tests
     if args.vm and args.test_name:
         test_data_dir = "/tmp/vpp-vm-tests"
@@ -353,7 +422,21 @@ if __name__ == "__main__":
     debug = False if args.release else True
     build_vpp(debug, args.release)
     set_environ()
-    if vm_tests:
+    if args.running_vpp:
+        print("Tests will be run against a running VPP..")
+    elif not vm_tests:
+        print("Tests will be run by spawning a new VPP instance..")
+    # Run tests against a running VPP or a new instance of VPP
+    if not vm_tests:
+        run_tests_in_venv(
+            test=args.test_name,
+            jobs=args.jobs,
+            log_dir=args.log_dir,
+            socket_dir=args.socket_dir,
+            running_vpp=args.running_vpp,
+        )
+    # Run tests against a VPP inside a VM
+    else:
         print("Running VPP unit test(s):{0} inside a QEMU VM".format(args.test_name))
         # Check Available CPUs & Usable Memory
         cpus = expand_mix_string(args.vm_cpu_list)
@@ -366,5 +449,10 @@ if __name__ == "__main__":
             print(f"Error: Mem Size:{args.vm_mem}G > Avail Mem:{avail_mem}G")
             sys.exit(1)
         vm_test_runner(
-            args.test_name, args.kernel_image, test_data_dir, cpus, f"{args.vm_mem}G"
+            args.test_name,
+            args.kernel_image,
+            test_data_dir,
+            cpus,
+            f"{args.vm_mem}G",
+            args.jobs,
         )
diff --git a/test/vpp_running.py b/test/vpp_running.py
new file mode 100644 (file)
index 0000000..e1ffe37
--- /dev/null
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+
+# Supporting module for running tests against a running VPP.
+# This module is used by the test framework. Do not invoke this module
+# directly for running tests against a running vpp. Use run.py for
+# running all unit tests.
+
+from glob import glob
+import os
+import sys
+import subprocess
+from config import config
+
+
+def use_running(cls):
+    """Update VPPTestCase to use running VPP's sock files & methods.
+
+    Arguments:
+    cls -- VPPTestCase Class
+    """
+    if config.running_vpp:
+        if os.path.isdir(config.socket_dir):
+            RunningVPP.socket_dir = config.socket_dir
+        else:
+            RunningVPP.socket_dir = RunningVPP.get_default_socket_dir()
+        RunningVPP.get_set_vpp_sock_files()
+        cls.get_stats_sock_path = RunningVPP.get_stats_sock_path
+        cls.get_api_sock_path = RunningVPP.get_api_sock_path
+        cls.run_vpp = RunningVPP.run_vpp
+        cls.quit_vpp = RunningVPP.quit_vpp
+        cls.vpp = RunningVPP
+        cls.running_vpp = True
+    return cls
+
+
+class RunningVPP:
+
+    api_sock = ""  # api_sock file path
+    stats_sock = ""  # stats sock_file path
+    socket_dir = ""  # running VPP's socket directory
+    pid = None  # running VPP's pid
+    returncode = None  # indicates to the framework that VPP is running
+
+    @classmethod
+    def get_stats_sock_path(cls):
+        return cls.stats_sock
+
+    @classmethod
+    def get_api_sock_path(cls):
+        return cls.api_sock
+
+    @classmethod
+    def run_vpp(cls):
+        """VPP is already running -- skip this action."""
+        pass
+
+    @classmethod
+    def quit_vpp(cls):
+        """Indicate quitting to framework by setting returncode=1."""
+        cls.returncode = 1
+
+    @classmethod
+    def terminate(cls):
+        """Indicate termination to framework by setting returncode=1."""
+        cls.returncode = 1
+
+    @classmethod
+    def get_default_socket_dir(cls):
+        """Return running VPP's default socket directory.
+
+        Default socket dir is:
+           /var/run/user/${UID}/vpp  (or)
+           /var/run/vpp, if VPP is started as a root user
+        """
+        if cls.is_running_vpp():
+            vpp_user_id = (
+                subprocess.check_output(["ps", "-o", "uid=", "-p", str(cls.pid)])
+                .decode("utf-8")
+                .strip()
+            )
+            if vpp_user_id == "0":
+                return "/var/run/vpp"
+            else:
+                return f"/var/run/user/{vpp_user_id}/vpp"
+        else:
+            print(
+                "Error: getting default socket dir, as "
+                "a running VPP process could not be found"
+            )
+            sys.exit(1)
+
+    @classmethod
+    def get_set_vpp_sock_files(cls):
+        """Look for *.sock files in the socket_dir and set cls attributes.
+
+        Returns a tuple: (api_sock_file, stats_sock_file)
+        Sets cls.api_sock and cls.stats_sock attributes
+        """
+        # Return if the sock files are already set
+        if cls.api_sock and cls.stats_sock:
+            return (cls.api_sock, cls.stats_sock)
+        # Find running VPP's sock files in the socket dir
+        if os.path.isdir(cls.socket_dir):
+            if not cls.is_running_vpp():
+                print(
+                    "Error: The socket dir for a running VPP directory is, "
+                    "set but a running VPP process could not be found"
+                )
+                sys.exit(1)
+            sock_files = glob(os.path.join(cls.socket_dir + "/" + "*.sock"))
+            for sock_file in sock_files:
+                if "api.sock" in sock_file:
+                    cls.api_sock = os.path.abspath(sock_file)
+                elif "stats.sock" in sock_file:
+                    cls.stats_sock = os.path.abspath(sock_file)
+            if not cls.api_sock:
+                print(
+                    f"Error: Could not find a valid api.sock file "
+                    f"in running VPP's socket directory {cls.socket_dir}"
+                )
+                sys.exit(1)
+            if not cls.stats_sock:
+                print(
+                    f"Error: Could not find a valid stats.sock file "
+                    f"in running VPP's socket directory {cls.socket_dir}"
+                )
+                sys.exit(1)
+            return (cls.api_sock, cls.stats_sock)
+        else:
+            print("Error: The socket dir for a running VPP directory is unset")
+            sys.exit(1)
+
+    @classmethod
+    def is_running_vpp(cls):
+        """Return True if VPP's pid is visible else False."""
+        vpp_pid = subprocess.Popen(
+            ["pgrep", "-d,", "-x", "vpp_main"],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            universal_newlines=True,
+        )
+        stdout, stderr = vpp_pid.communicate()
+        cls.pid = int(stdout.split(",")[0]) if stdout else None
+        return bool(cls.pid)
+
+    @classmethod
+    def poll(cls):
+        """Return None to indicate that the process hasn't terminated."""
+        return cls.returncode
+
+
+if __name__ == "__main__":
+    RunningVPP.socket_dir = RunningVPP.get_default_socket_dir()
+    RunningVPP.get_set_vpp_sock_files()
+    print(f"Running VPP's sock files")
+    print(f"api_sock_file {RunningVPP.api_sock}")
+    print(f"stats_sock_file {RunningVPP.stats_sock}")