tests: run tests against a running VPP

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>
This commit is contained in:
Naveen Joy
2022-08-30 13:59:03 -07:00
committed by Damjan Marion
parent 229f5fcf18
commit c872cec3f0
4 changed files with 291 additions and 12 deletions

View 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

View 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

View File

@ -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,
)

157
test/vpp_running.py Normal file
View File

@ -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}")