blob: 58112fd790cfcd21a5585795058fb3302a8340be [file] [log] [blame]
Naveen Joy7ea7ab52021-05-11 10:31:18 -07001#!/usr/bin/env python3
2#
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:
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
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.
15#
16# Build the Virtual Environment & run VPP unit tests
17
18import argparse
19import glob
20import logging
21import os
22from pathlib import Path
23import signal
Naveen Joyc872cec2022-08-30 13:59:03 -070024from subprocess import Popen, PIPE, STDOUT, call
Naveen Joy7ea7ab52021-05-11 10:31:18 -070025import sys
26import time
27import venv
Naveen Joy7498aad2022-09-20 11:38:33 -070028import datetime
Naveen Joy7ea7ab52021-05-11 10:31:18 -070029
30
31# Required Std. Path Variables
32test_dir = os.path.dirname(os.path.realpath(__file__))
33ws_root = os.path.dirname(test_dir)
34build_root = os.path.join(ws_root, "build-root")
Naveen Joyc872cec2022-08-30 13:59:03 -070035venv_dir = os.path.join(build_root, "test", "venv")
Naveen Joy7ea7ab52021-05-11 10:31:18 -070036venv_bin_dir = os.path.join(venv_dir, "bin")
37venv_lib_dir = os.path.join(venv_dir, "lib")
38venv_run_dir = os.path.join(venv_dir, "run")
39venv_install_done = os.path.join(venv_run_dir, "venv_install.done")
40papi_python_src_dir = os.path.join(ws_root, "src", "vpp-api", "python")
41
42# Path Variables Set after VPP Build/Install
43vpp_build_dir = vpp_install_path = vpp_bin = vpp_lib = vpp_lib64 = None
44vpp_plugin_path = vpp_test_plugin_path = ld_library_path = None
45
46# Pip version pinning
47pip_version = "22.0.4"
48pip_tools_version = "6.6.0"
49
50# Test requirement files
51test_requirements_file = os.path.join(test_dir, "requirements.txt")
52# Auto-generated requirement file
53pip_compiled_requirements_file = os.path.join(test_dir, "requirements-3.txt")
54
55
56# Gracefully exit after executing cleanup scripts
57# upon receiving a SIGINT or SIGTERM
58def handler(signum, frame):
59 print("Received Signal {0}".format(signum))
60 post_vm_test_run()
61
62
63signal.signal(signal.SIGINT, handler)
64signal.signal(signal.SIGTERM, handler)
65
66
67def show_progress(stream):
68 """
69 Read lines from a subprocess stdout/stderr streams and write
70 to sys.stdout & the logfile
71 """
72 while True:
73 s = stream.readline()
74 if not s:
75 break
76 data = s.decode("utf-8")
77 # Filter the annoying SIGTERM signal from the output when VPP is
78 # terminated after a test run
79 if "SIGTERM" not in data:
80 sys.stdout.write(data)
81 logging.debug(data)
82 sys.stdout.flush()
83 stream.close()
84
85
86class ExtendedEnvBuilder(venv.EnvBuilder):
87 """
88 1. Builds a Virtual Environment for running VPP unit tests
89 2. Installs all necessary scripts, pkgs & patches into the vEnv
90 - python3, pip, pip-tools, papi, scapy patches &
91 test-requirement pkgs
92 """
93
94 def __init__(self, *args, **kwargs):
95 super().__init__(*args, **kwargs)
96
97 def post_setup(self, context):
98 """
99 Setup all packages that need to be pre-installed into the venv
100 prior to running VPP unit tests.
101
102 :param context: The context of the virtual environment creation
103 request being processed.
104 """
105 os.environ["VIRTUAL_ENV"] = context.env_dir
106 os.environ[
107 "CUSTOM_COMPILE_COMMAND"
108 ] = "make test-refresh-deps (or update requirements.txt)"
109 # Cleanup previously auto-generated pip req. file
110 try:
111 os.unlink(pip_compiled_requirements_file)
112 except OSError:
113 pass
114 # Set the venv python executable & binary install path
115 env_exe = context.env_exe
116 bin_path = context.bin_path
117 # Packages/requirements to be installed in the venv
118 # [python-module, cmdline-args, package-name_or_requirements-file-name]
119 test_req = [
120 ["pip", "install", "pip===%s" % pip_version],
121 ["pip", "install", "pip-tools===%s" % pip_tools_version],
122 [
123 "piptools",
124 "compile",
125 "-q",
126 "--generate-hashes",
127 test_requirements_file,
128 "--output-file",
129 pip_compiled_requirements_file,
130 ],
131 ["piptools", "sync", pip_compiled_requirements_file],
132 ["pip", "install", "-e", papi_python_src_dir],
133 ]
134 for req in test_req:
135 args = [env_exe, "-m"]
136 args.extend(req)
137 print(args)
138 p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=bin_path)
139 show_progress(p.stdout)
140 self.pip_patch()
141
142 def pip_patch(self):
143 """
144 Apply scapy patch files
145 """
146 scapy_patch_dir = Path(os.path.join(test_dir, "patches", "scapy-2.4.3"))
147 scapy_source_dir = glob.glob(
148 os.path.join(venv_lib_dir, "python3.*", "site-packages")
149 )[0]
150 for f in scapy_patch_dir.iterdir():
151 print("Applying patch: {}".format(os.path.basename(str(f))))
152 args = ["patch", "--forward", "-p1", "-d", scapy_source_dir, "-i", str(f)]
153 print(args)
154 p = Popen(args, stdout=PIPE, stderr=STDOUT)
155 show_progress(p.stdout)
156
157
158# Build VPP Release/Debug binaries
159def build_vpp(debug=True, release=False):
160 """
161 Install VPP Release(if release=True) or Debug(if debug=True) Binaries.
162
163 Default is to build the debug binaries.
164 """
165 global vpp_build_dir, vpp_install_path, vpp_bin, vpp_lib, vpp_lib64
166 global vpp_plugin_path, vpp_test_plugin_path, ld_library_path
167 if debug:
168 print("Building VPP debug binaries")
169 args = ["make", "build"]
170 build = "build-vpp_debug-native"
171 install = "install-vpp_debug-native"
172 elif release:
173 print("Building VPP release binaries")
174 args = ["make", "build-release"]
175 build = "build-vpp-native"
176 install = "install-vpp-native"
177 p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=ws_root)
178 show_progress(p.stdout)
179 vpp_build_dir = os.path.join(build_root, build)
180 vpp_install_path = os.path.join(build_root, install)
181 vpp_bin = os.path.join(vpp_install_path, "vpp", "bin", "vpp")
182 vpp_lib = os.path.join(vpp_install_path, "vpp", "lib")
183 vpp_lib64 = os.path.join(vpp_install_path, "vpp", "lib64")
184 vpp_plugin_path = (
185 os.path.join(vpp_lib, "vpp_plugins")
186 + ":"
187 + os.path.join(vpp_lib64, "vpp_plugins")
188 )
189 vpp_test_plugin_path = (
190 os.path.join(vpp_lib, "vpp_api_test_plugins")
191 + ":"
192 + os.path.join(vpp_lib64, "vpp_api_test_plugins")
193 )
194 ld_library_path = os.path.join(vpp_lib) + ":" + os.path.join(vpp_lib64)
195
196
197# Environment Vars required by the test framework,
198# papi_provider & unittests
199def set_environ():
200 os.environ["WS_ROOT"] = ws_root
201 os.environ["BR"] = build_root
202 os.environ["VENV_PATH"] = venv_dir
203 os.environ["VENV_BIN"] = venv_bin_dir
204 os.environ["RND_SEED"] = str(time.time())
205 os.environ["VPP_BUILD_DIR"] = vpp_build_dir
206 os.environ["VPP_BIN"] = vpp_bin
207 os.environ["VPP_PLUGIN_PATH"] = vpp_plugin_path
208 os.environ["VPP_TEST_PLUGIN_PATH"] = vpp_test_plugin_path
209 os.environ["VPP_INSTALL_PATH"] = vpp_install_path
210 os.environ["LD_LIBRARY_PATH"] = ld_library_path
211 os.environ["FAILED_DIR"] = "/tmp/vpp-failed-unittests/"
212 if not os.environ.get("TEST_JOBS"):
213 os.environ["TEST_JOBS"] = "1"
214
215
216# Runs a test inside a spawned QEMU VM
217# If a kernel image is not provided, a linux-image-kvm image is
218# downloaded to the test_data_dir
Naveen Joyc872cec2022-08-30 13:59:03 -0700219def vm_test_runner(test_name, kernel_image, test_data_dir, cpu_mask, mem, jobs="auto"):
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700220 script = os.path.join(test_dir, "scripts", "run_vpp_in_vm.sh")
Naveen Joyc872cec2022-08-30 13:59:03 -0700221 os.environ["TEST_JOBS"] = str(jobs)
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700222 p = Popen(
223 [script, test_name, kernel_image, test_data_dir, cpu_mask, mem],
224 stdout=PIPE,
225 stderr=STDOUT,
226 cwd=ws_root,
227 )
228 show_progress(p.stdout)
229 post_vm_test_run()
230
231
232def post_vm_test_run():
233 # Revert the ownership of certain directories from root to the
234 # original user after running in QEMU
235 print("Running post test cleanup tasks")
236 dirs = ["/tmp/vpp-failed-unittests", os.path.join(ws_root, "test", "__pycache__")]
237 dirs.extend(glob.glob("/tmp/vpp-unittest-*"))
238 dirs.extend(glob.glob("/tmp/api_post_mortem.*"))
239 user = os.getlogin()
240 for dir in dirs:
241 if os.path.exists(dir) and Path(dir).owner() != user:
242 cmd = ["sudo", "chown", "-R", "{0}:{0}".format(user), dir]
243 p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
244 show_progress(p.stdout)
245
246
247def build_venv():
248 # Builds a virtual env containing all the required packages and patches
249 # for running VPP unit tests
250 if not os.path.exists(venv_install_done):
251 env_builder = ExtendedEnvBuilder(clear=True, with_pip=True)
252 print("Creating a vEnv for running VPP unit tests in {}".format(venv_dir))
253 env_builder.create(venv_dir)
254 # Write state to the venv run dir
255 Path(venv_run_dir).mkdir(exist_ok=True)
256 Path(venv_install_done).touch()
257
258
259def expand_mix_string(s):
260 # Returns an expanded string computed from a mixrange string (s)
261 # E.g: If param s = '5-8,10,11' returns '5,6,7,8,10,11'
262 result = []
263 for val in s.split(","):
264 if "-" in val:
265 start, end = val.split("-")
266 result.extend(list(range(int(start), int(end) + 1)))
267 else:
268 result.append(int(val))
269 return ",".join(str(i) for i in set(result))
270
271
272def set_logging(test_data_dir, test_name):
273 Path(test_data_dir).mkdir(exist_ok=True)
274 log_file = "vm_{0}_{1}.log".format(test_name, str(time.time())[-5:])
275 filename = "{0}/{1}".format(test_data_dir, log_file)
276 Path(filename).touch()
277 logging.basicConfig(filename=filename, level=logging.DEBUG)
278
279
Naveen Joyc872cec2022-08-30 13:59:03 -0700280def run_tests_in_venv(
281 test,
282 jobs,
283 log_dir,
284 socket_dir="",
285 running_vpp=False,
Naveen Joy5569a852022-10-17 15:07:49 -0700286 extended=False,
Naveen Joyc872cec2022-08-30 13:59:03 -0700287):
288 """Runs tests in the virtual environment set by venv_dir.
289
290 Arguments:
291 test: Name of the test to run
292 jobs: Maximum concurrent test jobs
293 log_dir: Directory location for storing log files
294 socket_dir: Use running VPP's socket files
295 running_vpp: True if tests are run against a running VPP
Naveen Joy5569a852022-10-17 15:07:49 -0700296 extended: Run extended tests
Naveen Joyc872cec2022-08-30 13:59:03 -0700297 """
298 script = os.path.join(test_dir, "scripts", "run.sh")
299 args = [
300 f"--venv-dir={venv_dir}",
301 f"--vpp-ws-dir={ws_root}",
302 f"--socket-dir={socket_dir}",
303 f"--filter={test}",
304 f"--jobs={jobs}",
305 f"--log-dir={log_dir}",
Naveen Joy7498aad2022-09-20 11:38:33 -0700306 f"--tmp-dir={log_dir}",
Naveen Joyc872cec2022-08-30 13:59:03 -0700307 ]
308 if running_vpp:
309 args = args + [f"--use-running-vpp"]
Naveen Joy5569a852022-10-17 15:07:49 -0700310 if extended:
311 args = args + [f"--extended"]
Naveen Joyc872cec2022-08-30 13:59:03 -0700312 print(f"Running script: {script} " f"{' '.join(args)}")
313 process_args = [script] + args
314 call(process_args)
315
316
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700317if __name__ == "__main__":
318 # Build a Virtual Environment for running tests on host & QEMU
Naveen Joyc872cec2022-08-30 13:59:03 -0700319 # (TODO): Create a single config object by merging the below args with
320 # config.py after gathering dev use-cases.
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700321 parser = argparse.ArgumentParser(
322 description="Run VPP Unit Tests", formatter_class=argparse.RawTextHelpFormatter
323 )
324 parser.add_argument(
325 "--vm",
326 dest="vm",
Naveen Joyc872cec2022-08-30 13:59:03 -0700327 required=False,
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700328 action="store_true",
329 help="Run Test Inside a QEMU VM",
330 )
331 parser.add_argument(
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700332 "--debug",
333 dest="debug",
334 required=False,
335 default=True,
336 action="store_true",
337 help="Run Tests on Debug Build",
338 )
339 parser.add_argument(
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700340 "--release",
341 dest="release",
342 required=False,
343 default=False,
344 action="store_true",
345 help="Run Tests on release Build",
346 )
347 parser.add_argument(
Naveen Joyc872cec2022-08-30 13:59:03 -0700348 "-t",
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700349 "--test",
350 dest="test_name",
351 required=False,
352 action="store",
353 default="",
Naveen Joyc872cec2022-08-30 13:59:03 -0700354 help="Test Name or Test filter",
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700355 )
356 parser.add_argument(
357 "--vm-kernel-image",
358 dest="kernel_image",
359 required=False,
360 action="store",
361 default="",
362 help="Kernel Image Selection to Boot",
363 )
364 parser.add_argument(
365 "--vm-cpu-list",
366 dest="vm_cpu_list",
367 required=False,
368 action="store",
369 default="5-8",
370 help="Set CPU Affinity\n"
371 "E.g. 5-7,10 will schedule on processors "
372 "#5, #6, #7 and #10. (Default: 5-8)",
373 )
374 parser.add_argument(
375 "--vm-mem",
376 dest="vm_mem",
377 required=False,
378 action="store",
379 default="2",
380 help="Guest Memory in Gibibytes\n" "E.g. 4 (Default: 2)",
381 )
Naveen Joyc872cec2022-08-30 13:59:03 -0700382 parser.add_argument(
383 "--log-dir",
384 action="store",
Naveen Joy7498aad2022-09-20 11:38:33 -0700385 default=os.path.abspath(f"./test-run-{datetime.date.today()}"),
Naveen Joyc872cec2022-08-30 13:59:03 -0700386 help="directory where to store directories "
Naveen Joy7498aad2022-09-20 11:38:33 -0700387 "containing log files (default: ./test-run-YYYY-MM-DD)",
Naveen Joyc872cec2022-08-30 13:59:03 -0700388 )
389 parser.add_argument(
390 "--jobs",
391 action="store",
392 default="auto",
393 help="maximum concurrent test jobs",
394 )
395 parser.add_argument(
396 "-r",
397 "--use-running-vpp",
398 dest="running_vpp",
399 required=False,
400 action="store_true",
401 default=False,
402 help="Runs tests against a running VPP.",
403 )
404 parser.add_argument(
405 "-d",
406 "--socket-dir",
407 dest="socket_dir",
408 required=False,
409 action="store",
410 default="",
411 help="Relative or absolute path of running VPP's socket directory "
412 "containing api.sock & stats.sock files.\n"
413 "Default: /var/run/vpp if VPP is started as the root user, else "
414 "/var/run/user/${uid}/vpp.",
415 )
Naveen Joy5569a852022-10-17 15:07:49 -0700416 parser.add_argument(
417 "-e",
418 "--extended",
419 dest="extended",
420 required=False,
421 action="store_true",
422 default=False,
423 help="Run extended tests.",
424 )
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700425 args = parser.parse_args()
Naveen Joyc872cec2022-08-30 13:59:03 -0700426 vm_tests = False
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700427 # Enable VM tests
428 if args.vm and args.test_name:
429 test_data_dir = "/tmp/vpp-vm-tests"
430 set_logging(test_data_dir, args.test_name)
431 vm_tests = True
432 elif args.vm and not args.test_name:
433 print("Error: The --test argument must be set for running VM tests")
434 sys.exit(1)
435 build_venv()
436 # Build VPP release or debug binaries
437 debug = False if args.release else True
438 build_vpp(debug, args.release)
439 set_environ()
Naveen Joyc872cec2022-08-30 13:59:03 -0700440 if args.running_vpp:
441 print("Tests will be run against a running VPP..")
442 elif not vm_tests:
443 print("Tests will be run by spawning a new VPP instance..")
444 # Run tests against a running VPP or a new instance of VPP
445 if not vm_tests:
446 run_tests_in_venv(
447 test=args.test_name,
448 jobs=args.jobs,
449 log_dir=args.log_dir,
450 socket_dir=args.socket_dir,
451 running_vpp=args.running_vpp,
Naveen Joy5569a852022-10-17 15:07:49 -0700452 extended=args.extended,
Naveen Joyc872cec2022-08-30 13:59:03 -0700453 )
454 # Run tests against a VPP inside a VM
455 else:
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700456 print("Running VPP unit test(s):{0} inside a QEMU VM".format(args.test_name))
457 # Check Available CPUs & Usable Memory
458 cpus = expand_mix_string(args.vm_cpu_list)
459 num_cpus, usable_cpus = (len(cpus.split(",")), len(os.sched_getaffinity(0)))
460 if num_cpus > usable_cpus:
461 print(f"Error:# of CPUs:{num_cpus} > Avail CPUs:{usable_cpus}")
462 sys.exit(1)
463 avail_mem = int(os.popen("free -t -g").readlines()[-1].split()[-1])
464 if int(args.vm_mem) > avail_mem:
465 print(f"Error: Mem Size:{args.vm_mem}G > Avail Mem:{avail_mem}G")
466 sys.exit(1)
467 vm_test_runner(
Naveen Joyc872cec2022-08-30 13:59:03 -0700468 args.test_name,
469 args.kernel_image,
470 test_data_dir,
471 cpus,
472 f"{args.vm_mem}G",
473 args.jobs,
Naveen Joy7ea7ab52021-05-11 10:31:18 -0700474 )