3 # Copyright (c) 2022 Cisco and/or its affiliates.
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
16 # Build the Virtual Environment & run VPP unit tests
22 from pathlib import Path
24 from subprocess import Popen, PIPE, STDOUT, call
30 # Required Std. Path Variables
31 test_dir = os.path.dirname(os.path.realpath(__file__))
32 ws_root = os.path.dirname(test_dir)
33 build_root = os.path.join(ws_root, "build-root")
34 venv_dir = os.path.join(build_root, "test", "venv")
35 venv_bin_dir = os.path.join(venv_dir, "bin")
36 venv_lib_dir = os.path.join(venv_dir, "lib")
37 venv_run_dir = os.path.join(venv_dir, "run")
38 venv_install_done = os.path.join(venv_run_dir, "venv_install.done")
39 papi_python_src_dir = os.path.join(ws_root, "src", "vpp-api", "python")
41 # Path Variables Set after VPP Build/Install
42 vpp_build_dir = vpp_install_path = vpp_bin = vpp_lib = vpp_lib64 = None
43 vpp_plugin_path = vpp_test_plugin_path = ld_library_path = None
46 pip_version = "22.0.4"
47 pip_tools_version = "6.6.0"
49 # Test requirement files
50 test_requirements_file = os.path.join(test_dir, "requirements.txt")
51 # Auto-generated requirement file
52 pip_compiled_requirements_file = os.path.join(test_dir, "requirements-3.txt")
55 # Gracefully exit after executing cleanup scripts
56 # upon receiving a SIGINT or SIGTERM
57 def handler(signum, frame):
58 print("Received Signal {0}".format(signum))
62 signal.signal(signal.SIGINT, handler)
63 signal.signal(signal.SIGTERM, handler)
66 def show_progress(stream):
68 Read lines from a subprocess stdout/stderr streams and write
69 to sys.stdout & the logfile
75 data = s.decode("utf-8")
76 # Filter the annoying SIGTERM signal from the output when VPP is
77 # terminated after a test run
78 if "SIGTERM" not in data:
79 sys.stdout.write(data)
85 class ExtendedEnvBuilder(venv.EnvBuilder):
87 1. Builds a Virtual Environment for running VPP unit tests
88 2. Installs all necessary scripts, pkgs & patches into the vEnv
89 - python3, pip, pip-tools, papi, scapy patches &
93 def __init__(self, *args, **kwargs):
94 super().__init__(*args, **kwargs)
96 def post_setup(self, context):
98 Setup all packages that need to be pre-installed into the venv
99 prior to running VPP unit tests.
101 :param context: The context of the virtual environment creation
102 request being processed.
104 os.environ["VIRTUAL_ENV"] = context.env_dir
106 "CUSTOM_COMPILE_COMMAND"
107 ] = "make test-refresh-deps (or update requirements.txt)"
108 # Cleanup previously auto-generated pip req. file
110 os.unlink(pip_compiled_requirements_file)
113 # Set the venv python executable & binary install path
114 env_exe = context.env_exe
115 bin_path = context.bin_path
116 # Packages/requirements to be installed in the venv
117 # [python-module, cmdline-args, package-name_or_requirements-file-name]
119 ["pip", "install", "pip===%s" % pip_version],
120 ["pip", "install", "pip-tools===%s" % pip_tools_version],
126 test_requirements_file,
128 pip_compiled_requirements_file,
130 ["piptools", "sync", pip_compiled_requirements_file],
131 ["pip", "install", "-e", papi_python_src_dir],
134 args = [env_exe, "-m"]
137 p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=bin_path)
138 show_progress(p.stdout)
143 Apply scapy patch files
145 scapy_patch_dir = Path(os.path.join(test_dir, "patches", "scapy-2.4.3"))
146 scapy_source_dir = glob.glob(
147 os.path.join(venv_lib_dir, "python3.*", "site-packages")
149 for f in scapy_patch_dir.iterdir():
150 print("Applying patch: {}".format(os.path.basename(str(f))))
151 args = ["patch", "--forward", "-p1", "-d", scapy_source_dir, "-i", str(f)]
153 p = Popen(args, stdout=PIPE, stderr=STDOUT)
154 show_progress(p.stdout)
157 # Build VPP Release/Debug binaries
158 def build_vpp(debug=True, release=False):
160 Install VPP Release(if release=True) or Debug(if debug=True) Binaries.
162 Default is to build the debug binaries.
164 global vpp_build_dir, vpp_install_path, vpp_bin, vpp_lib, vpp_lib64
165 global vpp_plugin_path, vpp_test_plugin_path, ld_library_path
167 print("Building VPP debug binaries")
168 args = ["make", "build"]
169 build = "build-vpp_debug-native"
170 install = "install-vpp_debug-native"
172 print("Building VPP release binaries")
173 args = ["make", "build-release"]
174 build = "build-vpp-native"
175 install = "install-vpp-native"
176 p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=ws_root)
177 show_progress(p.stdout)
178 vpp_build_dir = os.path.join(build_root, build)
179 vpp_install_path = os.path.join(build_root, install)
180 vpp_bin = os.path.join(vpp_install_path, "vpp", "bin", "vpp")
181 vpp_lib = os.path.join(vpp_install_path, "vpp", "lib")
182 vpp_lib64 = os.path.join(vpp_install_path, "vpp", "lib64")
184 os.path.join(vpp_lib, "vpp_plugins")
186 + os.path.join(vpp_lib64, "vpp_plugins")
188 vpp_test_plugin_path = (
189 os.path.join(vpp_lib, "vpp_api_test_plugins")
191 + os.path.join(vpp_lib64, "vpp_api_test_plugins")
193 ld_library_path = os.path.join(vpp_lib) + ":" + os.path.join(vpp_lib64)
196 # Environment Vars required by the test framework,
197 # papi_provider & unittests
199 os.environ["WS_ROOT"] = ws_root
200 os.environ["BR"] = build_root
201 os.environ["VENV_PATH"] = venv_dir
202 os.environ["VENV_BIN"] = venv_bin_dir
203 os.environ["RND_SEED"] = str(time.time())
204 os.environ["VPP_BUILD_DIR"] = vpp_build_dir
205 os.environ["VPP_BIN"] = vpp_bin
206 os.environ["VPP_PLUGIN_PATH"] = vpp_plugin_path
207 os.environ["VPP_TEST_PLUGIN_PATH"] = vpp_test_plugin_path
208 os.environ["VPP_INSTALL_PATH"] = vpp_install_path
209 os.environ["LD_LIBRARY_PATH"] = ld_library_path
210 os.environ["FAILED_DIR"] = "/tmp/vpp-failed-unittests/"
211 if not os.environ.get("TEST_JOBS"):
212 os.environ["TEST_JOBS"] = "1"
215 # Runs a test inside a spawned QEMU VM
216 # If a kernel image is not provided, a linux-image-kvm image is
217 # downloaded to the test_data_dir
218 def vm_test_runner(test_name, kernel_image, test_data_dir, cpu_mask, mem, jobs="auto"):
219 script = os.path.join(test_dir, "scripts", "run_vpp_in_vm.sh")
220 os.environ["TEST_JOBS"] = str(jobs)
222 [script, test_name, kernel_image, test_data_dir, cpu_mask, mem],
227 show_progress(p.stdout)
231 def post_vm_test_run():
232 # Revert the ownership of certain directories from root to the
233 # original user after running in QEMU
234 print("Running post test cleanup tasks")
235 dirs = ["/tmp/vpp-failed-unittests", os.path.join(ws_root, "test", "__pycache__")]
236 dirs.extend(glob.glob("/tmp/vpp-unittest-*"))
237 dirs.extend(glob.glob("/tmp/api_post_mortem.*"))
240 if os.path.exists(dir) and Path(dir).owner() != user:
241 cmd = ["sudo", "chown", "-R", "{0}:{0}".format(user), dir]
242 p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
243 show_progress(p.stdout)
247 # Builds a virtual env containing all the required packages and patches
248 # for running VPP unit tests
249 if not os.path.exists(venv_install_done):
250 env_builder = ExtendedEnvBuilder(clear=True, with_pip=True)
251 print("Creating a vEnv for running VPP unit tests in {}".format(venv_dir))
252 env_builder.create(venv_dir)
253 # Write state to the venv run dir
254 Path(venv_run_dir).mkdir(exist_ok=True)
255 Path(venv_install_done).touch()
258 def expand_mix_string(s):
259 # Returns an expanded string computed from a mixrange string (s)
260 # E.g: If param s = '5-8,10,11' returns '5,6,7,8,10,11'
262 for val in s.split(","):
264 start, end = val.split("-")
265 result.extend(list(range(int(start), int(end) + 1)))
267 result.append(int(val))
268 return ",".join(str(i) for i in set(result))
271 def set_logging(test_data_dir, test_name):
272 Path(test_data_dir).mkdir(exist_ok=True)
273 log_file = "vm_{0}_{1}.log".format(test_name, str(time.time())[-5:])
274 filename = "{0}/{1}".format(test_data_dir, log_file)
275 Path(filename).touch()
276 logging.basicConfig(filename=filename, level=logging.DEBUG)
279 def run_tests_in_venv(
286 """Runs tests in the virtual environment set by venv_dir.
289 test: Name of the test to run
290 jobs: Maximum concurrent test jobs
291 log_dir: Directory location for storing log files
292 socket_dir: Use running VPP's socket files
293 running_vpp: True if tests are run against a running VPP
295 script = os.path.join(test_dir, "scripts", "run.sh")
297 f"--venv-dir={venv_dir}",
298 f"--vpp-ws-dir={ws_root}",
299 f"--socket-dir={socket_dir}",
302 f"--log-dir={log_dir}",
305 args = args + [f"--use-running-vpp"]
306 print(f"Running script: {script} " f"{' '.join(args)}")
307 process_args = [script] + args
311 if __name__ == "__main__":
312 # Build a Virtual Environment for running tests on host & QEMU
313 # (TODO): Create a single config object by merging the below args with
314 # config.py after gathering dev use-cases.
315 parser = argparse.ArgumentParser(
316 description="Run VPP Unit Tests", formatter_class=argparse.RawTextHelpFormatter
323 help="Run Test Inside a QEMU VM",
331 help="Run Tests on Debug Build",
339 help="Run Tests on release Build",
348 help="Test Name or Test filter",
356 help="Kernel Image Selection to Boot",
364 help="Set CPU Affinity\n"
365 "E.g. 5-7,10 will schedule on processors "
366 "#5, #6, #7 and #10. (Default: 5-8)",
374 help="Guest Memory in Gibibytes\n" "E.g. 4 (Default: 2)",
380 help="directory where to store directories "
381 "containing log files (default: /tmp)",
387 help="maximum concurrent test jobs",
396 help="Runs tests against a running VPP.",
405 help="Relative or absolute path of running VPP's socket directory "
406 "containing api.sock & stats.sock files.\n"
407 "Default: /var/run/vpp if VPP is started as the root user, else "
408 "/var/run/user/${uid}/vpp.",
410 args = parser.parse_args()
413 if args.vm and args.test_name:
414 test_data_dir = "/tmp/vpp-vm-tests"
415 set_logging(test_data_dir, args.test_name)
417 elif args.vm and not args.test_name:
418 print("Error: The --test argument must be set for running VM tests")
421 # Build VPP release or debug binaries
422 debug = False if args.release else True
423 build_vpp(debug, args.release)
426 print("Tests will be run against a running VPP..")
428 print("Tests will be run by spawning a new VPP instance..")
429 # Run tests against a running VPP or a new instance of VPP
434 log_dir=args.log_dir,
435 socket_dir=args.socket_dir,
436 running_vpp=args.running_vpp,
438 # Run tests against a VPP inside a VM
440 print("Running VPP unit test(s):{0} inside a QEMU VM".format(args.test_name))
441 # Check Available CPUs & Usable Memory
442 cpus = expand_mix_string(args.vm_cpu_list)
443 num_cpus, usable_cpus = (len(cpus.split(",")), len(os.sched_getaffinity(0)))
444 if num_cpus > usable_cpus:
445 print(f"Error:# of CPUs:{num_cpus} > Avail CPUs:{usable_cpus}")
447 avail_mem = int(os.popen("free -t -g").readlines()[-1].split()[-1])
448 if int(args.vm_mem) > avail_mem:
449 print(f"Error: Mem Size:{args.vm_mem}G > Avail Mem:{avail_mem}G")