aboutsummaryrefslogtreecommitdiffstats
path: root/test/run.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/run.py')
-rwxr-xr-xtest/run.py472
1 files changed, 472 insertions, 0 deletions
diff --git a/test/run.py b/test/run.py
new file mode 100755
index 00000000000..66764b7ba97
--- /dev/null
+++ b/test/run.py
@@ -0,0 +1,472 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2022 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.
+#
+# Build the Virtual Environment & run VPP unit tests
+
+import argparse
+import glob
+import logging
+import os
+from pathlib import Path
+import signal
+from subprocess import Popen, PIPE, STDOUT, call
+import sys
+import time
+import venv
+import datetime
+import re
+
+
+# Required Std. Path Variables
+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(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")
+venv_install_done = os.path.join(venv_run_dir, "venv_install.done")
+papi_python_src_dir = os.path.join(ws_root, "src", "vpp-api", "python")
+
+# Path Variables Set after VPP Build/Install
+vpp_build_dir = vpp_install_path = vpp_bin = vpp_lib = vpp_lib64 = None
+vpp_plugin_path = vpp_test_plugin_path = ld_library_path = None
+
+# Pip version pinning
+pip_version = "22.0.4"
+pip_tools_version = "6.6.0"
+
+# Compiled pip requirements file
+pip_compiled_requirements_file = os.path.join(test_dir, "requirements-3.txt")
+
+
+# Gracefully exit after executing cleanup scripts
+# upon receiving a SIGINT or SIGTERM
+def handler(signum, frame):
+ print("Received Signal {0}".format(signum))
+ post_vm_test_run()
+
+
+signal.signal(signal.SIGINT, handler)
+signal.signal(signal.SIGTERM, handler)
+
+
+def show_progress(stream, exclude_pattern=None):
+ """
+ Read lines from a subprocess stdout/stderr streams and write
+ to sys.stdout & the logfile
+
+ arguments:
+ stream - subprocess stdout or stderr data stream
+ exclude_pattern - lines matching this reg-ex will be excluded
+ from stdout.
+ """
+ while True:
+ s = stream.readline()
+ if not s:
+ break
+ data = s.decode("utf-8")
+ # Filter the annoying SIGTERM signal from the output when VPP is
+ # terminated after a test run
+ if "SIGTERM" not in data:
+ if exclude_pattern is not None:
+ if bool(re.search(exclude_pattern, data)) is False:
+ sys.stdout.write(data)
+ else:
+ sys.stdout.write(data)
+ logging.debug(data)
+ sys.stdout.flush()
+ stream.close()
+
+
+class ExtendedEnvBuilder(venv.EnvBuilder):
+ """
+ 1. Builds a Virtual Environment for running VPP unit tests
+ 2. Installs all necessary scripts, pkgs & patches into the vEnv
+ - python3, pip, pip-tools, papi, scapy patches &
+ test-requirement pkgs
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ def post_setup(self, context):
+ """
+ Setup all packages that need to be pre-installed into the venv
+ prior to running VPP unit tests.
+
+ :param context: The context of the virtual environment creation
+ request being processed.
+ """
+ os.environ["VIRTUAL_ENV"] = context.env_dir
+ os.environ["CUSTOM_COMPILE_COMMAND"] = (
+ "make test-refresh-deps (or update requirements.txt)"
+ )
+ # Set the venv python executable & binary install path
+ env_exe = context.env_exe
+ bin_path = context.bin_path
+ # Packages/requirements to be installed in the venv
+ # [python-module, cmdline-args, package-name_or_requirements-file-name]
+ test_req = [
+ ["pip", "install", "pip===%s" % pip_version],
+ ["pip", "install", "pip-tools===%s" % pip_tools_version],
+ ["piptools", "sync", pip_compiled_requirements_file],
+ ["pip", "install", "-e", papi_python_src_dir],
+ ]
+ for req in test_req:
+ args = [env_exe, "-m"]
+ args.extend(req)
+ print(args)
+ p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=bin_path)
+ show_progress(p.stdout)
+ self.pip_patch()
+
+ def pip_patch(self):
+ """
+ Apply scapy patch files
+ """
+ scapy_patch_dir = Path(os.path.join(test_dir, "patches", "scapy-2.4.3"))
+ scapy_source_dir = glob.glob(
+ os.path.join(venv_lib_dir, "python3.*", "site-packages")
+ )[0]
+ for f in scapy_patch_dir.iterdir():
+ print("Applying patch: {}".format(os.path.basename(str(f))))
+ args = ["patch", "--forward", "-p1", "-d", scapy_source_dir, "-i", str(f)]
+ print(args)
+ p = Popen(args, stdout=PIPE, stderr=STDOUT)
+ show_progress(p.stdout)
+
+
+# Build VPP Release/Debug binaries
+def build_vpp(debug=True, release=False):
+ """
+ Install VPP Release(if release=True) or Debug(if debug=True) Binaries.
+
+ Default is to build the debug binaries.
+ """
+ global vpp_build_dir, vpp_install_path, vpp_bin, vpp_lib, vpp_lib64
+ global vpp_plugin_path, vpp_test_plugin_path, ld_library_path
+ if debug:
+ print("Building VPP debug binaries")
+ args = ["make", "build"]
+ build = "build-vpp_debug-native"
+ install = "install-vpp_debug-native"
+ elif release:
+ print("Building VPP release binaries")
+ args = ["make", "build-release"]
+ build = "build-vpp-native"
+ install = "install-vpp-native"
+ p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=ws_root)
+ show_progress(p.stdout)
+ vpp_build_dir = os.path.join(build_root, build)
+ vpp_install_path = os.path.join(build_root, install)
+ vpp_bin = os.path.join(vpp_install_path, "vpp", "bin", "vpp")
+ vpp_lib = os.path.join(vpp_install_path, "vpp", "lib")
+ vpp_lib64 = os.path.join(vpp_install_path, "vpp", "lib64")
+ vpp_plugin_path = (
+ os.path.join(vpp_lib, "vpp_plugins")
+ + ":"
+ + os.path.join(vpp_lib64, "vpp_plugins")
+ )
+ vpp_test_plugin_path = (
+ os.path.join(vpp_lib, "vpp_api_test_plugins")
+ + ":"
+ + os.path.join(vpp_lib64, "vpp_api_test_plugins")
+ )
+ ld_library_path = os.path.join(vpp_lib) + ":" + os.path.join(vpp_lib64)
+
+
+# Environment Vars required by the test framework,
+# papi_provider & unittests
+def set_environ():
+ os.environ["WS_ROOT"] = ws_root
+ os.environ["BR"] = build_root
+ os.environ["VENV_PATH"] = venv_dir
+ os.environ["VENV_BIN"] = venv_bin_dir
+ os.environ["RND_SEED"] = str(time.time())
+ os.environ["VPP_BUILD_DIR"] = vpp_build_dir
+ os.environ["VPP_BIN"] = vpp_bin
+ os.environ["VPP_PLUGIN_PATH"] = vpp_plugin_path
+ os.environ["VPP_TEST_PLUGIN_PATH"] = vpp_test_plugin_path
+ os.environ["VPP_INSTALL_PATH"] = vpp_install_path
+ os.environ["LD_LIBRARY_PATH"] = ld_library_path
+ os.environ["FAILED_DIR"] = "/tmp/vpp-failed-unittests/"
+ if not os.environ.get("TEST_JOBS"):
+ os.environ["TEST_JOBS"] = "1"
+
+
+# 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, 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,
+ cwd=ws_root,
+ )
+ # Show only the test result without clobbering the stdout.
+ # The VM console displays VPP stderr & Linux IPv6 netdev change
+ # messages, which is logged by default and can be excluded.
+ exclude_pattern = r"vpp\[\d+\]:|ADDRCONF\(NETDEV_CHANGE\):"
+ show_progress(p.stdout, exclude_pattern)
+ post_vm_test_run()
+
+
+def post_vm_test_run():
+ # Revert the ownership of certain directories from root to the
+ # original user after running in QEMU
+ print("Running post test cleanup tasks")
+ dirs = ["/tmp/vpp-failed-unittests", os.path.join(ws_root, "test", "__pycache__")]
+ dirs.extend(glob.glob("/tmp/vpp-unittest-*"))
+ dirs.extend(glob.glob("/tmp/api_post_mortem.*"))
+ user = os.getlogin()
+ for dir in dirs:
+ if os.path.exists(dir) and Path(dir).owner() != user:
+ cmd = ["sudo", "chown", "-R", "{0}:{0}".format(user), dir]
+ p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
+ show_progress(p.stdout)
+
+
+def build_venv():
+ # Builds a virtual env containing all the required packages and patches
+ # for running VPP unit tests
+ if not os.path.exists(venv_install_done):
+ env_builder = ExtendedEnvBuilder(clear=True, with_pip=True)
+ print("Creating a vEnv for running VPP unit tests in {}".format(venv_dir))
+ env_builder.create(venv_dir)
+ # Write state to the venv run dir
+ Path(venv_run_dir).mkdir(exist_ok=True)
+ Path(venv_install_done).touch()
+
+
+def expand_mix_string(s):
+ # Returns an expanded string computed from a mixrange string (s)
+ # E.g: If param s = '5-8,10,11' returns '5,6,7,8,10,11'
+ result = []
+ for val in s.split(","):
+ if "-" in val:
+ start, end = val.split("-")
+ result.extend(list(range(int(start), int(end) + 1)))
+ else:
+ result.append(int(val))
+ return ",".join(str(i) for i in set(result))
+
+
+def set_logging(test_data_dir, test_name):
+ Path(test_data_dir).mkdir(exist_ok=True)
+ log_file = "vm_{0}_{1}.log".format(test_name, str(time.time())[-5:])
+ filename = "{0}/{1}".format(test_data_dir, log_file)
+ Path(filename).touch()
+ logging.basicConfig(filename=filename, level=logging.DEBUG)
+
+
+def run_tests_in_venv(
+ test,
+ jobs,
+ log_dir,
+ socket_dir="",
+ running_vpp=False,
+ extended=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
+ extended: Run extended tests
+ """
+ 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}",
+ f"--tmp-dir={log_dir}",
+ f"--cache-vpp-output",
+ ]
+ if running_vpp:
+ args = args + [f"--use-running-vpp"]
+ if extended:
+ args = args + [f"--extended"]
+ 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=False,
+ action="store_true",
+ help="Run Test Inside a QEMU VM",
+ )
+ parser.add_argument(
+ "--debug",
+ dest="debug",
+ required=False,
+ default=True,
+ action="store_true",
+ help="Run Tests on Debug Build",
+ )
+ parser.add_argument(
+ "--release",
+ dest="release",
+ required=False,
+ default=False,
+ action="store_true",
+ help="Run Tests on release Build",
+ )
+ parser.add_argument(
+ "-t",
+ "--test",
+ dest="test_name",
+ required=False,
+ action="store",
+ default="",
+ help="Test Name or Test filter",
+ )
+ parser.add_argument(
+ "--vm-kernel-image",
+ dest="kernel_image",
+ required=False,
+ action="store",
+ default="",
+ help="Kernel Image Selection to Boot",
+ )
+ parser.add_argument(
+ "--vm-cpu-list",
+ dest="vm_cpu_list",
+ required=False,
+ action="store",
+ default="5-8",
+ help="Set CPU Affinity\n"
+ "E.g. 5-7,10 will schedule on processors "
+ "#5, #6, #7 and #10. (Default: 5-8)",
+ )
+ parser.add_argument(
+ "--vm-mem",
+ dest="vm_mem",
+ required=False,
+ action="store",
+ default="2",
+ help="Guest Memory in Gibibytes\n" "E.g. 4 (Default: 2)",
+ )
+ parser.add_argument(
+ "--log-dir",
+ action="store",
+ default=os.path.abspath(f"./test-run-{datetime.date.today()}"),
+ help="directory where to store directories "
+ "containing log files (default: ./test-run-YYYY-MM-DD)",
+ )
+ 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.",
+ )
+ parser.add_argument(
+ "-e",
+ "--extended",
+ dest="extended",
+ required=False,
+ action="store_true",
+ default=False,
+ help="Run extended tests.",
+ )
+ args = parser.parse_args()
+ vm_tests = False
+ # Enable VM tests
+ if args.vm and args.test_name:
+ test_data_dir = "/tmp/vpp-vm-tests"
+ set_logging(test_data_dir, args.test_name)
+ vm_tests = True
+ elif args.vm and not args.test_name:
+ print("Error: The --test argument must be set for running VM tests")
+ sys.exit(1)
+ build_venv()
+ # Build VPP release or debug binaries
+ debug = False if args.release else True
+ build_vpp(debug, args.release)
+ set_environ()
+ 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,
+ extended=args.extended,
+ )
+ # 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)
+ num_cpus, usable_cpus = (len(cpus.split(",")), len(os.sched_getaffinity(0)))
+ if num_cpus > usable_cpus:
+ print(f"Error:# of CPUs:{num_cpus} > Avail CPUs:{usable_cpus}")
+ sys.exit(1)
+ avail_mem = int(os.popen("free -t -g").readlines()[-1].split()[-1])
+ if int(args.vm_mem) > avail_mem:
+ 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.jobs,
+ )