blob: 07b24d55b829958013e2e362d8fa82c697774c89 [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
24from subprocess import Popen, PIPE, STDOUT
25import sys
26import time
27import venv
28
29
30# Required Std. Path Variables
31test_dir = os.path.dirname(os.path.realpath(__file__))
32ws_root = os.path.dirname(test_dir)
33build_root = os.path.join(ws_root, "build-root")
34venv_dir = os.path.join(test_dir, "venv")
35venv_bin_dir = os.path.join(venv_dir, "bin")
36venv_lib_dir = os.path.join(venv_dir, "lib")
37venv_run_dir = os.path.join(venv_dir, "run")
38venv_install_done = os.path.join(venv_run_dir, "venv_install.done")
39papi_python_src_dir = os.path.join(ws_root, "src", "vpp-api", "python")
40
41# Path Variables Set after VPP Build/Install
42vpp_build_dir = vpp_install_path = vpp_bin = vpp_lib = vpp_lib64 = None
43vpp_plugin_path = vpp_test_plugin_path = ld_library_path = None
44
45# Pip version pinning
46pip_version = "22.0.4"
47pip_tools_version = "6.6.0"
48
49# Test requirement files
50test_requirements_file = os.path.join(test_dir, "requirements.txt")
51# Auto-generated requirement file
52pip_compiled_requirements_file = os.path.join(test_dir, "requirements-3.txt")
53
54
55# Gracefully exit after executing cleanup scripts
56# upon receiving a SIGINT or SIGTERM
57def handler(signum, frame):
58 print("Received Signal {0}".format(signum))
59 post_vm_test_run()
60
61
62signal.signal(signal.SIGINT, handler)
63signal.signal(signal.SIGTERM, handler)
64
65
66def show_progress(stream):
67 """
68 Read lines from a subprocess stdout/stderr streams and write
69 to sys.stdout & the logfile
70 """
71 while True:
72 s = stream.readline()
73 if not s:
74 break
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)
80 logging.debug(data)
81 sys.stdout.flush()
82 stream.close()
83
84
85class ExtendedEnvBuilder(venv.EnvBuilder):
86 """
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 &
90 test-requirement pkgs
91 """
92
93 def __init__(self, *args, **kwargs):
94 super().__init__(*args, **kwargs)
95
96 def post_setup(self, context):
97 """
98 Setup all packages that need to be pre-installed into the venv
99 prior to running VPP unit tests.
100
101 :param context: The context of the virtual environment creation
102 request being processed.
103 """
104 os.environ["VIRTUAL_ENV"] = context.env_dir
105 os.environ[
106 "CUSTOM_COMPILE_COMMAND"
107 ] = "make test-refresh-deps (or update requirements.txt)"
108 # Cleanup previously auto-generated pip req. file
109 try:
110 os.unlink(pip_compiled_requirements_file)
111 except OSError:
112 pass
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]
118 test_req = [
119 ["pip", "install", "pip===%s" % pip_version],
120 ["pip", "install", "pip-tools===%s" % pip_tools_version],
121 [
122 "piptools",
123 "compile",
124 "-q",
125 "--generate-hashes",
126 test_requirements_file,
127 "--output-file",
128 pip_compiled_requirements_file,
129 ],
130 ["piptools", "sync", pip_compiled_requirements_file],
131 ["pip", "install", "-e", papi_python_src_dir],
132 ]
133 for req in test_req:
134 args = [env_exe, "-m"]
135 args.extend(req)
136 print(args)
137 p = Popen(args, stdout=PIPE, stderr=STDOUT, cwd=bin_path)
138 show_progress(p.stdout)
139 self.pip_patch()
140
141 def pip_patch(self):
142 """
143 Apply scapy patch files
144 """
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")
148 )[0]
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)]
152 print(args)
153 p = Popen(args, stdout=PIPE, stderr=STDOUT)
154 show_progress(p.stdout)
155
156
157# Build VPP Release/Debug binaries
158def build_vpp(debug=True, release=False):
159 """
160 Install VPP Release(if release=True) or Debug(if debug=True) Binaries.
161
162 Default is to build the debug binaries.
163 """
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
166 if debug:
167 print("Building VPP debug binaries")
168 args = ["make", "build"]
169 build = "build-vpp_debug-native"
170 install = "install-vpp_debug-native"
171 elif release:
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")
183 vpp_plugin_path = (
184 os.path.join(vpp_lib, "vpp_plugins")
185 + ":"
186 + os.path.join(vpp_lib64, "vpp_plugins")
187 )
188 vpp_test_plugin_path = (
189 os.path.join(vpp_lib, "vpp_api_test_plugins")
190 + ":"
191 + os.path.join(vpp_lib64, "vpp_api_test_plugins")
192 )
193 ld_library_path = os.path.join(vpp_lib) + ":" + os.path.join(vpp_lib64)
194
195
196# Environment Vars required by the test framework,
197# papi_provider & unittests
198def set_environ():
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"
213
214
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
218def vm_test_runner(test_name, kernel_image, test_data_dir, cpu_mask, mem):
219 script = os.path.join(test_dir, "scripts", "run_vpp_in_vm.sh")
220 p = Popen(
221 [script, test_name, kernel_image, test_data_dir, cpu_mask, mem],
222 stdout=PIPE,
223 stderr=STDOUT,
224 cwd=ws_root,
225 )
226 show_progress(p.stdout)
227 post_vm_test_run()
228
229
230def post_vm_test_run():
231 # Revert the ownership of certain directories from root to the
232 # original user after running in QEMU
233 print("Running post test cleanup tasks")
234 dirs = ["/tmp/vpp-failed-unittests", os.path.join(ws_root, "test", "__pycache__")]
235 dirs.extend(glob.glob("/tmp/vpp-unittest-*"))
236 dirs.extend(glob.glob("/tmp/api_post_mortem.*"))
237 user = os.getlogin()
238 for dir in dirs:
239 if os.path.exists(dir) and Path(dir).owner() != user:
240 cmd = ["sudo", "chown", "-R", "{0}:{0}".format(user), dir]
241 p = Popen(cmd, stdout=PIPE, stderr=STDOUT)
242 show_progress(p.stdout)
243
244
245def build_venv():
246 # Builds a virtual env containing all the required packages and patches
247 # for running VPP unit tests
248 if not os.path.exists(venv_install_done):
249 env_builder = ExtendedEnvBuilder(clear=True, with_pip=True)
250 print("Creating a vEnv for running VPP unit tests in {}".format(venv_dir))
251 env_builder.create(venv_dir)
252 # Write state to the venv run dir
253 Path(venv_run_dir).mkdir(exist_ok=True)
254 Path(venv_install_done).touch()
255
256
257def expand_mix_string(s):
258 # Returns an expanded string computed from a mixrange string (s)
259 # E.g: If param s = '5-8,10,11' returns '5,6,7,8,10,11'
260 result = []
261 for val in s.split(","):
262 if "-" in val:
263 start, end = val.split("-")
264 result.extend(list(range(int(start), int(end) + 1)))
265 else:
266 result.append(int(val))
267 return ",".join(str(i) for i in set(result))
268
269
270def set_logging(test_data_dir, test_name):
271 Path(test_data_dir).mkdir(exist_ok=True)
272 log_file = "vm_{0}_{1}.log".format(test_name, str(time.time())[-5:])
273 filename = "{0}/{1}".format(test_data_dir, log_file)
274 Path(filename).touch()
275 logging.basicConfig(filename=filename, level=logging.DEBUG)
276
277
278if __name__ == "__main__":
279 # Build a Virtual Environment for running tests on host & QEMU
280 parser = argparse.ArgumentParser(
281 description="Run VPP Unit Tests", formatter_class=argparse.RawTextHelpFormatter
282 )
283 parser.add_argument(
284 "--vm",
285 dest="vm",
286 required=True,
287 action="store_true",
288 help="Run Test Inside a QEMU VM",
289 )
290 parser.add_argument(
291 "-d",
292 "--debug",
293 dest="debug",
294 required=False,
295 default=True,
296 action="store_true",
297 help="Run Tests on Debug Build",
298 )
299 parser.add_argument(
300 "-r",
301 "--release",
302 dest="release",
303 required=False,
304 default=False,
305 action="store_true",
306 help="Run Tests on release Build",
307 )
308 parser.add_argument(
309 "--test",
310 dest="test_name",
311 required=False,
312 action="store",
313 default="",
314 help="Tests to Run",
315 )
316 parser.add_argument(
317 "--vm-kernel-image",
318 dest="kernel_image",
319 required=False,
320 action="store",
321 default="",
322 help="Kernel Image Selection to Boot",
323 )
324 parser.add_argument(
325 "--vm-cpu-list",
326 dest="vm_cpu_list",
327 required=False,
328 action="store",
329 default="5-8",
330 help="Set CPU Affinity\n"
331 "E.g. 5-7,10 will schedule on processors "
332 "#5, #6, #7 and #10. (Default: 5-8)",
333 )
334 parser.add_argument(
335 "--vm-mem",
336 dest="vm_mem",
337 required=False,
338 action="store",
339 default="2",
340 help="Guest Memory in Gibibytes\n" "E.g. 4 (Default: 2)",
341 )
342 args = parser.parse_args()
343 # Enable VM tests
344 if args.vm and args.test_name:
345 test_data_dir = "/tmp/vpp-vm-tests"
346 set_logging(test_data_dir, args.test_name)
347 vm_tests = True
348 elif args.vm and not args.test_name:
349 print("Error: The --test argument must be set for running VM tests")
350 sys.exit(1)
351 build_venv()
352 # Build VPP release or debug binaries
353 debug = False if args.release else True
354 build_vpp(debug, args.release)
355 set_environ()
356 if vm_tests:
357 print("Running VPP unit test(s):{0} inside a QEMU VM".format(args.test_name))
358 # Check Available CPUs & Usable Memory
359 cpus = expand_mix_string(args.vm_cpu_list)
360 num_cpus, usable_cpus = (len(cpus.split(",")), len(os.sched_getaffinity(0)))
361 if num_cpus > usable_cpus:
362 print(f"Error:# of CPUs:{num_cpus} > Avail CPUs:{usable_cpus}")
363 sys.exit(1)
364 avail_mem = int(os.popen("free -t -g").readlines()[-1].split()[-1])
365 if int(args.vm_mem) > avail_mem:
366 print(f"Error: Mem Size:{args.vm_mem}G > Avail Mem:{avail_mem}G")
367 sys.exit(1)
368 vm_test_runner(
369 args.test_name, args.kernel_image, test_data_dir, cpus, f"{args.vm_mem}G"
370 )