blob: 4cd4d94ab7b48dfebde2f492cc03e6b7f6ed94d9 [file] [log] [blame]
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001#!/usr/bin/env python3
2
3from __future__ import print_function
4import logging
5import sys
6import os
7import select
8import signal
9import subprocess
10import unittest
11import re
12import time
13import faulthandler
14import random
15import copy
16import platform
17import shutil
Dave Wallace8800f732023-08-31 00:47:44 -040018from pathlib import Path
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040019from collections import deque
20from threading import Thread, Event
21from inspect import getdoc, isclass
22from traceback import format_exception
23from logging import FileHandler, DEBUG, Formatter
24from enum import Enum
25from abc import ABC, abstractmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040026
Dave Wallace8800f732023-08-31 00:47:44 -040027from config import config, max_vpp_cpus
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040028import hook as hookmodule
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040029from vpp_lo_interface import VppLoInterface
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040030from vpp_papi_provider import VppPapiProvider
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040031import vpp_papi
32from vpp_papi.vpp_stats import VPPStats
33from vpp_papi.vpp_transport_socket import VppTransportSocketIOError
34from log import (
35 RED,
36 GREEN,
37 YELLOW,
38 double_line_delim,
39 single_line_delim,
40 get_logger,
41 colorize,
42)
43from vpp_object import VppObjectRegistry
Dave Wallace8800f732023-08-31 00:47:44 -040044from util import is_core_present
Klement Sekera47f35272023-03-29 16:04:58 +020045from test_result_code import TestResultCode
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040046
47logger = logging.getLogger(__name__)
48
49# Set up an empty logger for the testcase that can be overridden as necessary
Dave Wallace8800f732023-08-31 00:47:44 -040050null_logger = logging.getLogger("VppAsfTestCase")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040051null_logger.addHandler(logging.NullHandler())
52
Pratikshya Prasai657bdf72022-08-18 11:09:38 -040053
54if config.debug_framework:
55 import debug_internal
56
57"""
58 Test framework module.
59
60 The module provides a set of tools for constructing and running tests and
61 representing the results.
62"""
63
64
65class VppDiedError(Exception):
66 """exception for reporting that the subprocess has died."""
67
68 signals_by_value = {
69 v: k
70 for k, v in signal.__dict__.items()
71 if k.startswith("SIG") and not k.startswith("SIG_")
72 }
73
74 def __init__(self, rv=None, testcase=None, method_name=None):
75 self.rv = rv
76 self.signal_name = None
77 self.testcase = testcase
78 self.method_name = method_name
79
80 try:
81 self.signal_name = VppDiedError.signals_by_value[-rv]
82 except (KeyError, TypeError):
83 pass
84
85 if testcase is None and method_name is None:
86 in_msg = ""
87 else:
88 in_msg = " while running %s.%s" % (testcase, method_name)
89
90 if self.rv:
91 msg = "VPP subprocess died unexpectedly%s with return code: %d%s." % (
92 in_msg,
93 self.rv,
94 " [%s]" % (self.signal_name if self.signal_name is not None else ""),
95 )
96 else:
97 msg = "VPP subprocess died unexpectedly%s." % in_msg
98
99 super(VppDiedError, self).__init__(msg)
100
101
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400102def pump_output(testclass):
103 """pump output from vpp stdout/stderr to proper queues"""
104 stdout_fragment = ""
105 stderr_fragment = ""
106 while not testclass.pump_thread_stop_flag.is_set():
107 readable = select.select(
108 [
109 testclass.vpp.stdout.fileno(),
110 testclass.vpp.stderr.fileno(),
111 testclass.pump_thread_wakeup_pipe[0],
112 ],
113 [],
114 [],
115 )[0]
116 if testclass.vpp.stdout.fileno() in readable:
117 read = os.read(testclass.vpp.stdout.fileno(), 102400)
118 if len(read) > 0:
119 split = read.decode("ascii", errors="backslashreplace").splitlines(True)
120 if len(stdout_fragment) > 0:
121 split[0] = "%s%s" % (stdout_fragment, split[0])
122 if len(split) > 0 and split[-1].endswith("\n"):
123 limit = None
124 else:
125 limit = -1
126 stdout_fragment = split[-1]
127 testclass.vpp_stdout_deque.extend(split[:limit])
128 if not config.cache_vpp_output:
129 for line in split[:limit]:
130 testclass.logger.info("VPP STDOUT: %s" % line.rstrip("\n"))
131 if testclass.vpp.stderr.fileno() in readable:
132 read = os.read(testclass.vpp.stderr.fileno(), 102400)
133 if len(read) > 0:
134 split = read.decode("ascii", errors="backslashreplace").splitlines(True)
135 if len(stderr_fragment) > 0:
136 split[0] = "%s%s" % (stderr_fragment, split[0])
137 if len(split) > 0 and split[-1].endswith("\n"):
138 limit = None
139 else:
140 limit = -1
141 stderr_fragment = split[-1]
142
143 testclass.vpp_stderr_deque.extend(split[:limit])
144 if not config.cache_vpp_output:
145 for line in split[:limit]:
146 testclass.logger.error("VPP STDERR: %s" % line.rstrip("\n"))
147 # ignoring the dummy pipe here intentionally - the
148 # flag will take care of properly terminating the loop
149
150
151def _is_platform_aarch64():
152 return platform.machine() == "aarch64"
153
154
155is_platform_aarch64 = _is_platform_aarch64()
156
157
Dave Wallace8800f732023-08-31 00:47:44 -0400158def _is_distro_ubuntu2204():
159 with open("/etc/os-release") as f:
160 for line in f.readlines():
161 if "jammy" in line:
162 return True
163 return False
164
165
166is_distro_ubuntu2204 = _is_distro_ubuntu2204()
167
168
169def _is_distro_debian11():
170 with open("/etc/os-release") as f:
171 for line in f.readlines():
172 if "bullseye" in line:
173 return True
174 return False
175
176
177is_distro_debian11 = _is_distro_debian11()
178
179
180def _is_distro_ubuntu2204():
181 with open("/etc/os-release") as f:
182 for line in f.readlines():
183 if "jammy" in line:
184 return True
185 return False
186
187
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400188class KeepAliveReporter(object):
189 """
190 Singleton object which reports test start to parent process
191 """
192
193 _shared_state = {}
194
195 def __init__(self):
196 self.__dict__ = self._shared_state
197 self._pipe = None
198
199 @property
200 def pipe(self):
201 return self._pipe
202
203 @pipe.setter
204 def pipe(self, pipe):
205 if self._pipe is not None:
206 raise Exception("Internal error - pipe should only be set once.")
207 self._pipe = pipe
208
209 def send_keep_alive(self, test, desc=None):
210 """
211 Write current test tmpdir & desc to keep-alive pipe to signal liveness
212 """
213 if self.pipe is None:
214 # if not running forked..
215 return
216
217 if isclass(test):
218 desc = "%s (%s)" % (desc, unittest.util.strclass(test))
219 else:
220 desc = test.id()
221
222 self.pipe.send((desc, config.vpp, test.tempdir, test.vpp.pid))
223
224
225class TestCaseTag(Enum):
226 # marks the suites that must run at the end
227 # using only a single test runner
228 RUN_SOLO = 1
229 # marks the suites broken on VPP multi-worker
230 FIXME_VPP_WORKERS = 2
231 # marks the suites broken when ASan is enabled
232 FIXME_ASAN = 3
Dave Wallace8800f732023-08-31 00:47:44 -0400233 # marks suites broken on Ubuntu-22.04
234 FIXME_UBUNTU2204 = 4
235 # marks suites broken on Debian-11
236 FIXME_DEBIAN11 = 5
237 # marks suites broken on debug vpp image
238 FIXME_VPP_DEBUG = 6
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400239
240
241def create_tag_decorator(e):
242 def decorator(cls):
243 try:
244 cls.test_tags.append(e)
245 except AttributeError:
246 cls.test_tags = [e]
247 return cls
248
249 return decorator
250
251
252tag_run_solo = create_tag_decorator(TestCaseTag.RUN_SOLO)
253tag_fixme_vpp_workers = create_tag_decorator(TestCaseTag.FIXME_VPP_WORKERS)
254tag_fixme_asan = create_tag_decorator(TestCaseTag.FIXME_ASAN)
Dave Wallace8800f732023-08-31 00:47:44 -0400255tag_fixme_ubuntu2204 = create_tag_decorator(TestCaseTag.FIXME_UBUNTU2204)
256tag_fixme_debian11 = create_tag_decorator(TestCaseTag.FIXME_DEBIAN11)
257tag_fixme_vpp_debug = create_tag_decorator(TestCaseTag.FIXME_VPP_DEBUG)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400258
259
260class DummyVpp:
261 returncode = None
262 pid = 0xCAFEBAFE
263
264 def poll(self):
265 pass
266
267 def terminate(self):
268 pass
269
270
271class CPUInterface(ABC):
272 cpus = []
273 skipped_due_to_cpu_lack = False
274
275 @classmethod
276 @abstractmethod
277 def get_cpus_required(cls):
278 pass
279
280 @classmethod
281 def assign_cpus(cls, cpus):
282 cls.cpus = cpus
283
284
Dave Wallace8800f732023-08-31 00:47:44 -0400285class VppAsfTestCase(CPUInterface, unittest.TestCase):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400286 """This subclass is a base class for VPP test cases that are implemented as
287 classes. It provides methods to create and run test case.
288 """
289
290 extra_vpp_statseg_config = ""
Klement Sekerad3e0d102023-01-26 12:35:35 +0100291 extra_vpp_config = []
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400292 extra_vpp_plugin_config = []
293 logger = null_logger
294 vapi_response_timeout = 5
295 remove_configured_vpp_objects_on_tear_down = True
296
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400297 @classmethod
298 def has_tag(cls, tag):
299 """if the test case has a given tag - return true"""
300 try:
301 return tag in cls.test_tags
302 except AttributeError:
303 pass
304 return False
305
306 @classmethod
307 def is_tagged_run_solo(cls):
308 """if the test case class is timing-sensitive - return true"""
309 return cls.has_tag(TestCaseTag.RUN_SOLO)
310
311 @classmethod
312 def skip_fixme_asan(cls):
313 """if @tag_fixme_asan & ASan is enabled - mark for skip"""
314 if cls.has_tag(TestCaseTag.FIXME_ASAN):
315 vpp_extra_cmake_args = os.environ.get("VPP_EXTRA_CMAKE_ARGS", "")
316 if "DVPP_ENABLE_SANITIZE_ADDR=ON" in vpp_extra_cmake_args:
317 cls = unittest.skip("Skipping @tag_fixme_asan tests")(cls)
318
319 @classmethod
Andrew Yourtchenko9987d472024-07-02 18:02:09 +0200320 def skip_fixme_ubuntu2204(cls):
321 """if @tag_fixme_ubuntu2204 & is Ubuntu22.04 - mark for skip"""
322 if cls.has_tag(TestCaseTag.FIXME_UBUNTU2204) and is_distro_ubuntu2204 == True:
323 cls = unittest.skip("Skipping @tag_fixme_ubuntu2204 tests")(cls)
324
325 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400326 def instance(cls):
327 """Return the instance of this testcase"""
328 return cls.test_instance
329
330 @classmethod
331 def set_debug_flags(cls, d):
332 cls.gdbserver_port = 7777
333 cls.debug_core = False
334 cls.debug_gdb = False
335 cls.debug_gdbserver = False
336 cls.debug_all = False
337 cls.debug_attach = False
338 if d is None:
339 return
340 dl = d.lower()
341 if dl == "core":
342 cls.debug_core = True
343 elif dl == "gdb" or dl == "gdb-all":
344 cls.debug_gdb = True
345 elif dl == "gdbserver" or dl == "gdbserver-all":
346 cls.debug_gdbserver = True
347 elif dl == "attach":
348 cls.debug_attach = True
349 else:
350 raise Exception("Unrecognized DEBUG option: '%s'" % d)
351 if dl == "gdb-all" or dl == "gdbserver-all":
352 cls.debug_all = True
353
354 @classmethod
355 def get_vpp_worker_count(cls):
356 if not hasattr(cls, "vpp_worker_count"):
357 if cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
358 cls.vpp_worker_count = 0
359 else:
360 cls.vpp_worker_count = config.vpp_worker_count
361 return cls.vpp_worker_count
362
363 @classmethod
364 def get_cpus_required(cls):
365 return 1 + cls.get_vpp_worker_count()
366
367 @classmethod
368 def setUpConstants(cls):
369 """Set-up the test case class based on environment variables"""
370 cls.step = config.step
371 cls.plugin_path = ":".join(config.vpp_plugin_dir)
372 cls.test_plugin_path = ":".join(config.vpp_test_plugin_dir)
373 cls.extern_plugin_path = ":".join(config.extern_plugin_dir)
374 debug_cli = ""
375 if cls.step or cls.debug_gdb or cls.debug_gdbserver:
376 debug_cli = "cli-listen localhost:5002"
377 size = re.search(r"\d+[gG]", config.coredump_size)
378 if size:
379 coredump_size = f"coredump-size {config.coredump_size}".lower()
380 else:
381 coredump_size = "coredump-size unlimited"
382 default_variant = config.variant
383 if default_variant is not None:
Dmitry Valter953fbd22021-09-19 23:50:57 +0300384 default_variant = "default { variant %s 100 }" % default_variant
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400385 else:
386 default_variant = ""
387
388 api_fuzzing = config.api_fuzz
389 if api_fuzzing is None:
390 api_fuzzing = "off"
391
392 cls.vpp_cmdline = [
393 config.vpp,
394 "unix",
395 "{",
396 "nodaemon",
397 debug_cli,
398 "full-coredump",
399 coredump_size,
400 "runtime-dir",
401 cls.tempdir,
402 "}",
403 "api-trace",
404 "{",
405 "on",
406 "}",
407 "api-segment",
408 "{",
409 "prefix",
410 cls.get_api_segment_prefix(),
411 "}",
412 "cpu",
413 "{",
414 "main-core",
415 str(cls.cpus[0]),
416 ]
417 if cls.extern_plugin_path not in (None, ""):
418 cls.extra_vpp_plugin_config.append("add-path %s" % cls.extern_plugin_path)
419 if cls.get_vpp_worker_count():
420 cls.vpp_cmdline.extend(
421 ["corelist-workers", ",".join([str(x) for x in cls.cpus[1:]])]
422 )
423 cls.vpp_cmdline.extend(
424 [
425 "}",
426 "physmem",
427 "{",
428 "max-size",
429 "32m",
430 "}",
431 "statseg",
432 "{",
433 "socket-name",
434 cls.get_stats_sock_path(),
435 cls.extra_vpp_statseg_config,
436 "}",
437 "socksvr",
438 "{",
439 "socket-name",
440 cls.get_api_sock_path(),
441 "}",
442 "node { ",
443 default_variant,
444 "}",
445 "api-fuzz {",
446 api_fuzzing,
447 "}",
448 "plugins",
449 "{",
450 "plugin",
451 "dpdk_plugin.so",
452 "{",
453 "disable",
454 "}",
455 "plugin",
456 "rdma_plugin.so",
457 "{",
458 "disable",
459 "}",
460 "plugin",
461 "lisp_unittest_plugin.so",
462 "{",
463 "enable",
464 "}",
465 "plugin",
466 "unittest_plugin.so",
467 "{",
468 "enable",
469 "}",
470 ]
471 + cls.extra_vpp_plugin_config
472 + [
473 "}",
474 ]
475 )
476
Klement Sekerad3e0d102023-01-26 12:35:35 +0100477 if cls.extra_vpp_config is not None:
478 cls.vpp_cmdline.extend(cls.extra_vpp_config)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400479
480 if not cls.debug_attach:
481 cls.logger.info("vpp_cmdline args: %s" % cls.vpp_cmdline)
482 cls.logger.info("vpp_cmdline: %s" % " ".join(cls.vpp_cmdline))
483
484 @classmethod
485 def wait_for_enter(cls):
486 if cls.debug_gdbserver:
487 print(double_line_delim)
488 print("Spawned GDB server with PID: %d" % cls.vpp.pid)
489 elif cls.debug_gdb:
490 print(double_line_delim)
491 print("Spawned VPP with PID: %d" % cls.vpp.pid)
492 else:
493 cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid)
494 return
495 print(single_line_delim)
496 print("You can debug VPP using:")
497 if cls.debug_gdbserver:
498 print(
499 f"sudo gdb {config.vpp} "
500 f"-ex 'target remote localhost:{cls.gdbserver_port}'"
501 )
502 print(
503 "Now is the time to attach gdb by running the above "
504 "command, set up breakpoints etc., then resume VPP from "
505 "within gdb by issuing the 'continue' command"
506 )
507 cls.gdbserver_port += 1
508 elif cls.debug_gdb:
509 print(f"sudo gdb {config.vpp} -ex 'attach {cls.vpp.pid}'")
510 print(
511 "Now is the time to attach gdb by running the above "
512 "command and set up breakpoints etc., then resume VPP from"
513 " within gdb by issuing the 'continue' command"
514 )
515 print(single_line_delim)
516 input("Press ENTER to continue running the testcase...")
517
518 @classmethod
519 def attach_vpp(cls):
520 cls.vpp = DummyVpp()
521
522 @classmethod
523 def run_vpp(cls):
524 cls.logger.debug(f"Assigned cpus: {cls.cpus}")
525 cmdline = cls.vpp_cmdline
526
527 if cls.debug_gdbserver:
528 gdbserver = "/usr/bin/gdbserver"
529 if not os.path.isfile(gdbserver) or not os.access(gdbserver, os.X_OK):
530 raise Exception(
531 "gdbserver binary '%s' does not exist or is "
532 "not executable" % gdbserver
533 )
534
535 cmdline = [
536 gdbserver,
537 "localhost:{port}".format(port=cls.gdbserver_port),
538 ] + cls.vpp_cmdline
539 cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline))
540
541 try:
542 cls.vpp = subprocess.Popen(
543 cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE
544 )
545 except subprocess.CalledProcessError as e:
546 cls.logger.critical(
547 "Subprocess returned with non-0 return code: (%s)", e.returncode
548 )
549 raise
550 except OSError as e:
551 cls.logger.critical(
552 "Subprocess returned with OS error: (%s) %s", e.errno, e.strerror
553 )
554 raise
555 except Exception as e:
556 cls.logger.exception("Subprocess returned unexpected from %s:", cmdline)
557 raise
558
559 cls.wait_for_enter()
560
561 @classmethod
562 def wait_for_coredump(cls):
563 corefile = cls.tempdir + "/core"
564 if os.path.isfile(corefile):
565 cls.logger.error("Waiting for coredump to complete: %s", corefile)
566 curr_size = os.path.getsize(corefile)
567 deadline = time.time() + 60
568 ok = False
569 while time.time() < deadline:
570 cls.sleep(1)
571 size = curr_size
572 curr_size = os.path.getsize(corefile)
573 if size == curr_size:
574 ok = True
575 break
576 if not ok:
577 cls.logger.error(
578 "Timed out waiting for coredump to complete: %s", corefile
579 )
580 else:
581 cls.logger.error("Coredump complete: %s, size %d", corefile, curr_size)
582
583 @classmethod
584 def get_stats_sock_path(cls):
585 return "%s/stats.sock" % cls.tempdir
586
587 @classmethod
588 def get_api_sock_path(cls):
589 return "%s/api.sock" % cls.tempdir
590
591 @classmethod
Naveen Joy70335e82023-07-28 16:33:30 -0700592 def get_memif_sock_path(cls):
593 return "%s/memif.sock" % cls.tempdir
594
595 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400596 def get_api_segment_prefix(cls):
597 return os.path.basename(cls.tempdir) # Only used for VAPI
598
599 @classmethod
600 def get_tempdir(cls):
601 if cls.debug_attach:
602 tmpdir = f"{config.tmp_dir}/unittest-attach-gdb"
603 else:
Dave Wallace8800f732023-08-31 00:47:44 -0400604 tmpdir = f"{config.tmp_dir}/{get_testcase_dirname(cls.__name__)}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400605 if config.wipe_tmp_dir:
606 shutil.rmtree(tmpdir, ignore_errors=True)
607 os.mkdir(tmpdir)
608 return tmpdir
609
610 @classmethod
611 def create_file_handler(cls):
612 if config.log_dir is None:
613 cls.file_handler = FileHandler(f"{cls.tempdir}/log.txt")
614 return
615
Dave Wallace8800f732023-08-31 00:47:44 -0400616 logdir = f"{config.log_dir}/{get_testcase_dirname(cls.__name__)}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400617 if config.wipe_tmp_dir:
618 shutil.rmtree(logdir, ignore_errors=True)
619 os.mkdir(logdir)
620 cls.file_handler = FileHandler(f"{logdir}/log.txt")
621
622 @classmethod
623 def setUpClass(cls):
624 """
625 Perform class setup before running the testcase
626 Remove shared memory files, start vpp and connect the vpp-api
627 """
Dave Wallace8800f732023-08-31 00:47:44 -0400628 super(VppAsfTestCase, cls).setUpClass()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400629 cls.logger = get_logger(cls.__name__)
Dave Wallace8800f732023-08-31 00:47:44 -0400630 cls.logger.debug(f"--- START setUpClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400631 random.seed(config.rnd_seed)
632 if hasattr(cls, "parallel_handler"):
633 cls.logger.addHandler(cls.parallel_handler)
634 cls.logger.propagate = False
635 cls.set_debug_flags(config.debug)
636 cls.tempdir = cls.get_tempdir()
637 cls.create_file_handler()
638 cls.file_handler.setFormatter(
639 Formatter(fmt="%(asctime)s,%(msecs)03d %(message)s", datefmt="%H:%M:%S")
640 )
641 cls.file_handler.setLevel(DEBUG)
642 cls.logger.addHandler(cls.file_handler)
643 cls.logger.debug("--- setUpClass() for %s called ---" % cls.__name__)
644 os.chdir(cls.tempdir)
645 cls.logger.info(
646 "Temporary dir is %s, api socket is %s",
647 cls.tempdir,
648 cls.get_api_sock_path(),
649 )
650 cls.logger.debug("Random seed is %s", config.rnd_seed)
651 cls.setUpConstants()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400652 cls.verbose = 0
653 cls.vpp_dead = False
654 cls.registry = VppObjectRegistry()
655 cls.vpp_startup_failed = False
656 cls.reporter = KeepAliveReporter()
657 # need to catch exceptions here because if we raise, then the cleanup
658 # doesn't get called and we might end with a zombie vpp
659 try:
660 if cls.debug_attach:
661 cls.attach_vpp()
662 else:
663 cls.run_vpp()
664 cls.reporter.send_keep_alive(cls, "setUpClass")
665 VppTestResult.current_test_case_info = TestCaseInfo(
666 cls.logger, cls.tempdir, cls.vpp.pid, config.vpp
667 )
668 cls.vpp_stdout_deque = deque()
669 cls.vpp_stderr_deque = deque()
670 if not cls.debug_attach:
671 cls.pump_thread_stop_flag = Event()
672 cls.pump_thread_wakeup_pipe = os.pipe()
673 cls.pump_thread = Thread(target=pump_output, args=(cls,))
674 cls.pump_thread.daemon = True
675 cls.pump_thread.start()
676 if cls.debug_gdb or cls.debug_gdbserver or cls.debug_attach:
677 cls.vapi_response_timeout = 0
678 cls.vapi = VppPapiProvider(cls.__name__, cls, cls.vapi_response_timeout)
679 if cls.step:
680 hook = hookmodule.StepHook(cls)
681 else:
682 hook = hookmodule.PollHook(cls)
683 cls.vapi.register_hook(hook)
684 cls.statistics = VPPStats(socketname=cls.get_stats_sock_path())
685 try:
686 hook.poll_vpp()
687 except VppDiedError:
Dave Wallace8800f732023-08-31 00:47:44 -0400688 cls.wait_for_coredump()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400689 cls.vpp_startup_failed = True
690 cls.logger.critical(
691 "VPP died shortly after startup, check the"
692 " output to standard error for possible cause"
693 )
694 raise
695 try:
696 cls.vapi.connect()
697 except (vpp_papi.VPPIOError, Exception) as e:
698 cls.logger.debug("Exception connecting to vapi: %s" % e)
699 cls.vapi.disconnect()
700
701 if cls.debug_gdbserver:
702 print(
703 colorize(
704 "You're running VPP inside gdbserver but "
705 "VPP-API connection failed, did you forget "
706 "to 'continue' VPP from within gdb?",
707 RED,
708 )
709 )
710 raise e
711 if cls.debug_attach:
712 last_line = cls.vapi.cli("show thread").split("\n")[-2]
713 cls.vpp_worker_count = int(last_line.split(" ")[0])
714 print("Detected VPP with %s workers." % cls.vpp_worker_count)
715 except vpp_papi.VPPRuntimeError as e:
716 cls.logger.debug("%s" % e)
717 cls.quit()
718 raise e
719 except Exception as e:
720 cls.logger.debug("Exception connecting to VPP: %s" % e)
721 cls.quit()
722 raise e
Dave Wallace8800f732023-08-31 00:47:44 -0400723 cls.logger.debug(f"--- END setUpClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400724
725 @classmethod
726 def _debug_quit(cls):
727 if cls.debug_gdbserver or cls.debug_gdb:
728 try:
729 cls.vpp.poll()
730
731 if cls.vpp.returncode is None:
732 print()
733 print(double_line_delim)
734 print("VPP or GDB server is still running")
735 print(single_line_delim)
736 input(
737 "When done debugging, press ENTER to kill the "
738 "process and finish running the testcase..."
739 )
740 except AttributeError:
741 pass
742
743 @classmethod
744 def quit(cls):
745 """
746 Disconnect vpp-api, kill vpp and cleanup shared memory files
747 """
748 cls._debug_quit()
749
750 # first signal that we want to stop the pump thread, then wake it up
751 if hasattr(cls, "pump_thread_stop_flag"):
752 cls.pump_thread_stop_flag.set()
753 if hasattr(cls, "pump_thread_wakeup_pipe"):
754 os.write(cls.pump_thread_wakeup_pipe[1], b"ding dong wake up")
755 if hasattr(cls, "pump_thread"):
756 cls.logger.debug("Waiting for pump thread to stop")
757 cls.pump_thread.join()
758 if hasattr(cls, "vpp_stderr_reader_thread"):
759 cls.logger.debug("Waiting for stderr pump to stop")
760 cls.vpp_stderr_reader_thread.join()
761
762 if hasattr(cls, "vpp"):
763 if hasattr(cls, "vapi"):
764 cls.logger.debug(cls.vapi.vpp.get_stats())
765 cls.logger.debug("Disconnecting class vapi client on %s", cls.__name__)
766 cls.vapi.disconnect()
767 cls.logger.debug("Deleting class vapi attribute on %s", cls.__name__)
768 del cls.vapi
769 cls.vpp.poll()
770 if not cls.debug_attach and cls.vpp.returncode is None:
771 cls.wait_for_coredump()
772 cls.logger.debug("Sending TERM to vpp")
773 cls.vpp.terminate()
774 cls.logger.debug("Waiting for vpp to die")
775 try:
776 outs, errs = cls.vpp.communicate(timeout=5)
777 except subprocess.TimeoutExpired:
778 cls.vpp.kill()
779 outs, errs = cls.vpp.communicate()
780 cls.logger.debug("Deleting class vpp attribute on %s", cls.__name__)
781 if not cls.debug_attach:
782 cls.vpp.stdout.close()
783 cls.vpp.stderr.close()
784 del cls.vpp
785
786 if cls.vpp_startup_failed:
787 stdout_log = cls.logger.info
788 stderr_log = cls.logger.critical
789 else:
790 stdout_log = cls.logger.info
791 stderr_log = cls.logger.info
792
793 if hasattr(cls, "vpp_stdout_deque"):
794 stdout_log(single_line_delim)
795 stdout_log("VPP output to stdout while running %s:", cls.__name__)
796 stdout_log(single_line_delim)
797 vpp_output = "".join(cls.vpp_stdout_deque)
798 with open(cls.tempdir + "/vpp_stdout.txt", "w") as f:
799 f.write(vpp_output)
800 stdout_log("\n%s", vpp_output)
801 stdout_log(single_line_delim)
802
803 if hasattr(cls, "vpp_stderr_deque"):
804 stderr_log(single_line_delim)
805 stderr_log("VPP output to stderr while running %s:", cls.__name__)
806 stderr_log(single_line_delim)
807 vpp_output = "".join(cls.vpp_stderr_deque)
808 with open(cls.tempdir + "/vpp_stderr.txt", "w") as f:
809 f.write(vpp_output)
810 stderr_log("\n%s", vpp_output)
811 stderr_log(single_line_delim)
812
813 @classmethod
814 def tearDownClass(cls):
815 """Perform final cleanup after running all tests in this test-case"""
Dave Wallace8800f732023-08-31 00:47:44 -0400816 cls.logger.debug(f"--- START tearDownClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400817 cls.reporter.send_keep_alive(cls, "tearDownClass")
818 cls.quit()
819 cls.file_handler.close()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400820 if config.debug_framework:
821 debug_internal.on_tear_down_class(cls)
Dave Wallace8800f732023-08-31 00:47:44 -0400822 cls.logger.debug(f"--- END tearDownClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400823
824 def show_commands_at_teardown(self):
825 """Allow subclass specific teardown logging additions."""
826 self.logger.info("--- No test specific show commands provided. ---")
827
Dave Wallace87a88262023-11-27 20:23:03 -0500828 def unlink_testcase_file(self, path):
829 MAX_ATTEMPTS = 9
830 retries = MAX_ATTEMPTS
831 while retries > 0:
832 retries = retries - 1
833 self.logger.debug(f"Unlinking {path}")
834 try:
835 path.unlink()
836 # Loop until unlink() fails with FileNotFoundError to ensure file is removed
837 except FileNotFoundError:
838 break
839 except OSError:
840 self.logger.debug(f"OSError: unlinking {path}")
841 self.sleep(0.25, f"{retries} retries left")
842 if retries == 0 and os.path.isfile(path):
843 self.logger.error(
844 f"Unable to delete testcase file in {MAX_ATTEMPTS} attempts: {path}"
845 )
846 raise OSError
847
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400848 def tearDown(self):
849 """Show various debug prints after each test"""
850 self.logger.debug(
Dave Wallace8800f732023-08-31 00:47:44 -0400851 f"--- START tearDown() {self.__class__.__name__}.{self._testMethodName}({self._testMethodDoc}) ---"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400852 )
853
854 try:
855 if not self.vpp_dead:
856 self.logger.debug(self.vapi.cli("show trace max 1000"))
857 self.logger.info(self.vapi.ppcli("show interface"))
858 self.logger.info(self.vapi.ppcli("show hardware"))
859 self.logger.info(self.statistics.set_errors_str())
860 self.logger.info(self.vapi.ppcli("show run"))
861 self.logger.info(self.vapi.ppcli("show log"))
862 self.logger.info(self.vapi.ppcli("show bihash"))
863 self.logger.info("Logging testcase specific show commands.")
864 self.show_commands_at_teardown()
865 if self.remove_configured_vpp_objects_on_tear_down:
866 self.registry.remove_vpp_config(self.logger)
867 # Save/Dump VPP api trace log
868 m = self._testMethodName
869 api_trace = "vpp_api_trace.%s.%d.log" % (m, self.vpp.pid)
870 tmp_api_trace = "/tmp/%s" % api_trace
871 vpp_api_trace_log = "%s/%s" % (self.tempdir, api_trace)
872 self.logger.info(self.vapi.ppcli("api trace save %s" % api_trace))
873 self.logger.info("Moving %s to %s\n" % (tmp_api_trace, vpp_api_trace_log))
Dmitry Valter71d02aa2023-01-27 12:49:55 +0000874 shutil.move(tmp_api_trace, vpp_api_trace_log)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400875 except VppTransportSocketIOError:
876 self.logger.debug(
877 "VppTransportSocketIOError: Vpp dead. Cannot log show commands."
878 )
879 self.vpp_dead = True
880 else:
881 self.registry.unregister_all(self.logger)
Dave Wallace8800f732023-08-31 00:47:44 -0400882 # Remove any leftover pcap files
883 if hasattr(self, "pg_interfaces") and len(self.pg_interfaces) > 0:
884 testcase_dir = os.path.dirname(self.pg_interfaces[0].out_path)
885 for p in Path(testcase_dir).glob("pg*.pcap"):
Dave Wallace87a88262023-11-27 20:23:03 -0500886 self.unlink_testcase_file(p)
Dave Wallace8800f732023-08-31 00:47:44 -0400887 self.logger.debug(
888 f"--- END tearDown() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
889 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400890
891 def setUp(self):
892 """Clear trace before running each test"""
Dave Wallace8800f732023-08-31 00:47:44 -0400893 super(VppAsfTestCase, self).setUp()
894 self.logger.debug(
895 f"--- START setUp() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
896 )
897 # Save testname include in pcap history filenames
898 if hasattr(self, "pg_interfaces"):
899 for i in self.pg_interfaces:
900 i.test_name = self._testMethodName
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400901 self.reporter.send_keep_alive(self)
902 if self.vpp_dead:
Dave Wallace8800f732023-08-31 00:47:44 -0400903 self.wait_for_coredump()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400904 raise VppDiedError(
905 rv=None,
906 testcase=self.__class__.__name__,
907 method_name=self._testMethodName,
908 )
909 self.sleep(0.1, "during setUp")
910 self.vpp_stdout_deque.append(
911 "--- test setUp() for %s.%s(%s) starts here ---\n"
912 % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)
913 )
914 self.vpp_stderr_deque.append(
915 "--- test setUp() for %s.%s(%s) starts here ---\n"
916 % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)
917 )
918 self.vapi.cli("clear trace")
919 # store the test instance inside the test class - so that objects
920 # holding the class can access instance methods (like assertEqual)
921 type(self).test_instance = self
Dave Wallace8800f732023-08-31 00:47:44 -0400922 self.logger.debug(
923 f"--- END setUp() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
924 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400925
926 @classmethod
927 def get_vpp_time(cls):
928 # processes e.g. "Time now 2.190522, Wed, 11 Mar 2020 17:29:54 GMT"
929 # returns float("2.190522")
930 timestr = cls.vapi.cli("show clock")
931 head, sep, tail = timestr.partition(",")
932 head, sep, tail = head.partition("Time now")
933 return float(tail)
934
935 @classmethod
936 def sleep_on_vpp_time(cls, sec):
937 """Sleep according to time in VPP world"""
938 # On a busy system with many processes
939 # we might end up with VPP time being slower than real world
940 # So take that into account when waiting for VPP to do something
941 start_time = cls.get_vpp_time()
942 while cls.get_vpp_time() - start_time < sec:
943 cls.sleep(0.1)
944
945 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400946 def create_loopback_interfaces(cls, count):
947 """
948 Create loopback interfaces.
949
950 :param count: number of interfaces created.
951 :returns: List of created interfaces.
952 """
953 result = [VppLoInterface(cls) for i in range(count)]
954 for intf in result:
955 setattr(cls, intf.name, intf)
956 cls.lo_interfaces = result
957 return result
958
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400959 def assert_equal(self, real_value, expected_value, name_or_class=None):
960 if name_or_class is None:
961 self.assertEqual(real_value, expected_value)
962 return
963 try:
964 msg = "Invalid %s: %d('%s') does not match expected value %d('%s')"
965 msg = msg % (
966 getdoc(name_or_class).strip(),
967 real_value,
968 str(name_or_class(real_value)),
969 expected_value,
970 str(name_or_class(expected_value)),
971 )
972 except Exception:
973 msg = "Invalid %s: %s does not match expected value %s" % (
974 name_or_class,
975 real_value,
976 expected_value,
977 )
978
979 self.assertEqual(real_value, expected_value, msg)
980
981 def assert_in_range(self, real_value, expected_min, expected_max, name=None):
982 if name is None:
983 msg = None
984 else:
985 msg = "Invalid %s: %s out of range <%s,%s>" % (
986 name,
987 real_value,
988 expected_min,
989 expected_max,
990 )
991 self.assertTrue(expected_min <= real_value <= expected_max, msg)
992
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400993 def get_counter(self, counter):
994 if counter.startswith("/"):
995 counter_value = self.statistics.get_counter(counter)
996 else:
997 counters = self.vapi.cli("sh errors").split("\n")
998 counter_value = 0
999 for i in range(1, len(counters) - 1):
1000 results = counters[i].split()
1001 if results[1] == counter:
1002 counter_value = int(results[0])
1003 break
1004 return counter_value
1005
1006 def assert_counter_equal(self, counter, expected_value, thread=None, index=0):
1007 c = self.get_counter(counter)
1008 if thread is not None:
1009 c = c[thread][index]
1010 else:
1011 c = sum(x[index] for x in c)
Klement Sekerac5fa5392023-08-17 19:38:34 +02001012 self.logger.debug(
1013 "validate counter `%s[%s]', expected: %s, real value: %s"
1014 % (counter, index, expected_value, c)
1015 )
1016 self.assert_equal(c, expected_value, "counter `%s[%s]'" % (counter, index))
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001017
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001018 def assert_error_counter_equal(self, counter, expected_value):
1019 counter_value = self.statistics[counter].sum()
1020 self.assert_equal(counter_value, expected_value, "error counter `%s'" % counter)
1021
1022 @classmethod
1023 def sleep(cls, timeout, remark=None):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001024 # /* Allow sleep(0) to maintain win32 semantics, and as decreed
1025 # * by Guido, only the main thread can be interrupted.
1026 # */
1027 # https://github.com/python/cpython/blob/6673decfa0fb078f60587f5cb5e98460eea137c2/Modules/timemodule.c#L1892 # noqa
1028 if timeout == 0:
1029 # yield quantum
1030 if hasattr(os, "sched_yield"):
1031 os.sched_yield()
1032 else:
1033 time.sleep(0)
1034 return
1035
1036 cls.logger.debug("Starting sleep for %es (%s)", timeout, remark)
1037 before = time.time()
1038 time.sleep(timeout)
1039 after = time.time()
1040 if after - before > 2 * timeout:
1041 cls.logger.error(
1042 "unexpected self.sleep() result - slept for %es instead of ~%es!",
1043 after - before,
1044 timeout,
1045 )
1046
1047 cls.logger.debug(
1048 "Finished sleep (%s) - slept %es (wanted %es)",
1049 remark,
1050 after - before,
1051 timeout,
1052 )
1053
1054 def virtual_sleep(self, timeout, remark=None):
1055 self.logger.debug("Moving VPP time by %s (%s)", timeout, remark)
1056 self.vapi.cli("set clock adjust %s" % timeout)
1057
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001058 def snapshot_stats(self, stats_diff):
1059 """Return snapshot of interesting stats based on diff dictionary."""
1060 stats_snapshot = {}
1061 for sw_if_index in stats_diff:
1062 for counter in stats_diff[sw_if_index]:
1063 stats_snapshot[counter] = self.statistics[counter]
1064 self.logger.debug(f"Took statistics stats_snapshot: {stats_snapshot}")
1065 return stats_snapshot
1066
1067 def compare_stats_with_snapshot(self, stats_diff, stats_snapshot):
1068 """Assert appropriate difference between current stats and snapshot."""
1069 for sw_if_index in stats_diff:
1070 for cntr, diff in stats_diff[sw_if_index].items():
1071 if sw_if_index == "err":
1072 self.assert_equal(
1073 self.statistics[cntr].sum(),
1074 stats_snapshot[cntr].sum() + diff,
1075 f"'{cntr}' counter value (previous value: "
1076 f"{stats_snapshot[cntr].sum()}, "
1077 f"expected diff: {diff})",
1078 )
1079 else:
1080 try:
1081 self.assert_equal(
1082 self.statistics[cntr][:, sw_if_index].sum(),
1083 stats_snapshot[cntr][:, sw_if_index].sum() + diff,
1084 f"'{cntr}' counter value (previous value: "
1085 f"{stats_snapshot[cntr][:, sw_if_index].sum()}, "
1086 f"expected diff: {diff})",
1087 )
Klement Sekera01578852023-01-26 13:14:01 +01001088 except IndexError as e:
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001089 # if diff is 0, then this most probably a case where
1090 # test declares multiple interfaces but traffic hasn't
1091 # passed through this one yet - which means the counter
1092 # value is 0 and can be ignored
1093 if 0 != diff:
Klement Sekera01578852023-01-26 13:14:01 +01001094 raise Exception(
1095 f"Couldn't sum counter: {cntr} on sw_if_index: {sw_if_index}"
1096 ) from e
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001097
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001098
1099def get_testcase_doc_name(test):
1100 return getdoc(test.__class__).splitlines()[0]
1101
1102
1103def get_test_description(descriptions, test):
1104 short_description = test.shortDescription()
1105 if descriptions and short_description:
1106 return short_description
1107 else:
1108 return str(test)
1109
1110
Dave Wallace8800f732023-08-31 00:47:44 -04001111def get_failed_testcase_linkname(failed_dir, testcase_dirname):
1112 return os.path.join(failed_dir, f"{testcase_dirname}-FAILED")
1113
1114
1115def get_testcase_dirname(testcase_class_name):
1116 return f"vpp-unittest-{testcase_class_name}"
1117
1118
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001119class TestCaseInfo(object):
1120 def __init__(self, logger, tempdir, vpp_pid, vpp_bin_path):
1121 self.logger = logger
1122 self.tempdir = tempdir
1123 self.vpp_pid = vpp_pid
1124 self.vpp_bin_path = vpp_bin_path
1125 self.core_crash_test = None
1126
1127
1128class VppTestResult(unittest.TestResult):
1129 """
1130 @property result_string
1131 String variable to store the test case result string.
1132 @property errors
1133 List variable containing 2-tuples of TestCase instances and strings
1134 holding formatted tracebacks. Each tuple represents a test which
1135 raised an unexpected exception.
1136 @property failures
1137 List variable containing 2-tuples of TestCase instances and strings
1138 holding formatted tracebacks. Each tuple represents a test where
1139 a failure was explicitly signalled using the TestCase.assert*()
1140 methods.
1141 """
1142
1143 failed_test_cases_info = set()
1144 core_crash_test_cases_info = set()
1145 current_test_case_info = None
1146
1147 def __init__(self, stream=None, descriptions=None, verbosity=None, runner=None):
1148 """
1149 :param stream File descriptor to store where to report test results.
1150 Set to the standard error stream by default.
1151 :param descriptions Boolean variable to store information if to use
1152 test case descriptions.
1153 :param verbosity Integer variable to store required verbosity level.
1154 """
1155 super(VppTestResult, self).__init__(stream, descriptions, verbosity)
1156 self.stream = stream
1157 self.descriptions = descriptions
1158 self.verbosity = verbosity
Klement Sekera47f35272023-03-29 16:04:58 +02001159 self.result_code = TestResultCode.TEST_RUN
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001160 self.result_string = None
1161 self.runner = runner
1162 self.printed = []
1163
Dave Wallace8800f732023-08-31 00:47:44 -04001164 def decodePcapFiles(self, test, when_configured=False):
1165 if when_configured == False or config.decode_pcaps == True:
1166 if hasattr(test, "pg_interfaces") and len(test.pg_interfaces) > 0:
1167 testcase_dir = os.path.dirname(test.pg_interfaces[0].out_path)
1168 test.pg_interfaces[0].decode_pcap_files(
1169 testcase_dir, f"suite{test.__class__.__name__}"
1170 )
1171 test.pg_interfaces[0].decode_pcap_files(
1172 testcase_dir, test._testMethodName
1173 )
1174
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001175 def addSuccess(self, test):
1176 """
1177 Record a test succeeded result
1178
1179 :param test:
1180
1181 """
Klement Sekera47f35272023-03-29 16:04:58 +02001182 self.log_result("addSuccess", test)
Dave Wallace8800f732023-08-31 00:47:44 -04001183 self.decodePcapFiles(test, when_configured=True)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001184 unittest.TestResult.addSuccess(self, test)
1185 self.result_string = colorize("OK", GREEN)
Klement Sekera47f35272023-03-29 16:04:58 +02001186 self.result_code = TestResultCode.PASS
1187 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001188
Klement Sekera47f35272023-03-29 16:04:58 +02001189 def addExpectedFailure(self, test, err):
1190 self.log_result("addExpectedFailure", test, err)
Dave Wallace8800f732023-08-31 00:47:44 -04001191 self.decodePcapFiles(test)
Klement Sekera47f35272023-03-29 16:04:58 +02001192 super().addExpectedFailure(test, err)
1193 self.result_string = colorize("FAIL", GREEN)
1194 self.result_code = TestResultCode.EXPECTED_FAIL
1195 self.send_result_through_pipe(test, self.result_code)
1196
1197 def addUnexpectedSuccess(self, test):
1198 self.log_result("addUnexpectedSuccess", test)
Dave Wallace8800f732023-08-31 00:47:44 -04001199 self.decodePcapFiles(test, when_configured=True)
Klement Sekera47f35272023-03-29 16:04:58 +02001200 super().addUnexpectedSuccess(test)
1201 self.result_string = colorize("OK", RED)
1202 self.result_code = TestResultCode.UNEXPECTED_PASS
1203 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001204
1205 def addSkip(self, test, reason):
1206 """
1207 Record a test skipped.
1208
1209 :param test:
1210 :param reason:
1211
1212 """
Klement Sekera47f35272023-03-29 16:04:58 +02001213 self.log_result("addSkip", test, reason=reason)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001214 unittest.TestResult.addSkip(self, test, reason)
1215 self.result_string = colorize("SKIP", YELLOW)
1216
1217 if reason == "not enough cpus":
Klement Sekera47f35272023-03-29 16:04:58 +02001218 self.result_code = TestResultCode.SKIP_CPU_SHORTAGE
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001219 else:
Klement Sekera47f35272023-03-29 16:04:58 +02001220 self.result_code = TestResultCode.SKIP
1221 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001222
1223 def symlink_failed(self):
1224 if self.current_test_case_info:
1225 try:
1226 failed_dir = config.failed_dir
Dave Wallace8800f732023-08-31 00:47:44 -04001227 link_path = get_failed_testcase_linkname(
1228 failed_dir, os.path.basename(self.current_test_case_info.tempdir)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001229 )
1230
1231 self.current_test_case_info.logger.debug(
1232 "creating a link to the failed test"
1233 )
1234 self.current_test_case_info.logger.debug(
1235 "os.symlink(%s, %s)"
1236 % (self.current_test_case_info.tempdir, link_path)
1237 )
1238 if os.path.exists(link_path):
1239 self.current_test_case_info.logger.debug("symlink already exists")
1240 else:
1241 os.symlink(self.current_test_case_info.tempdir, link_path)
1242
1243 except Exception as e:
1244 self.current_test_case_info.logger.error(e)
1245
1246 def send_result_through_pipe(self, test, result):
1247 if hasattr(self, "test_framework_result_pipe"):
1248 pipe = self.test_framework_result_pipe
1249 if pipe:
1250 pipe.send((test.id(), result))
1251
Klement Sekera47f35272023-03-29 16:04:58 +02001252 def log_result(self, fn, test, err=None, reason=None):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001253 if self.current_test_case_info:
1254 if isinstance(test, unittest.suite._ErrorHolder):
1255 test_name = test.description
1256 else:
1257 test_name = "%s.%s(%s)" % (
1258 test.__class__.__name__,
1259 test._testMethodName,
1260 test._testMethodDoc,
1261 )
Klement Sekera47f35272023-03-29 16:04:58 +02001262 extra_msg = ""
1263 if err:
1264 extra_msg += f", error is {err}"
1265 if reason:
1266 extra_msg += f", reason is {reason}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001267 self.current_test_case_info.logger.debug(
Klement Sekera47f35272023-03-29 16:04:58 +02001268 f"--- {fn}() {test_name} called{extra_msg}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001269 )
Klement Sekera47f35272023-03-29 16:04:58 +02001270 if err:
1271 self.current_test_case_info.logger.debug(
1272 "formatted exception is:\n%s" % "".join(format_exception(*err))
1273 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001274
Klement Sekera47f35272023-03-29 16:04:58 +02001275 def add_error(self, test, err, unittest_fn, result_code):
1276 self.result_code = result_code
1277 if result_code == TestResultCode.FAIL:
1278 self.log_result("addFailure", test, err=err)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001279 error_type_str = colorize("FAIL", RED)
Klement Sekera47f35272023-03-29 16:04:58 +02001280 elif result_code == TestResultCode.ERROR:
1281 self.log_result("addError", test, err=err)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001282 error_type_str = colorize("ERROR", RED)
1283 else:
Klement Sekera47f35272023-03-29 16:04:58 +02001284 raise Exception(f"Unexpected result code {result_code}")
Dave Wallace8800f732023-08-31 00:47:44 -04001285 self.decodePcapFiles(test)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001286
1287 unittest_fn(self, test, err)
1288 if self.current_test_case_info:
1289 self.result_string = "%s [ temp dir used by test case: %s ]" % (
1290 error_type_str,
1291 self.current_test_case_info.tempdir,
1292 )
1293 self.symlink_failed()
1294 self.failed_test_cases_info.add(self.current_test_case_info)
1295 if is_core_present(self.current_test_case_info.tempdir):
1296 if not self.current_test_case_info.core_crash_test:
1297 if isinstance(test, unittest.suite._ErrorHolder):
1298 test_name = str(test)
1299 else:
1300 test_name = "'{!s}' ({!s})".format(
1301 get_testcase_doc_name(test), test.id()
1302 )
1303 self.current_test_case_info.core_crash_test = test_name
1304 self.core_crash_test_cases_info.add(self.current_test_case_info)
1305 else:
1306 self.result_string = "%s [no temp dir]" % error_type_str
1307
Klement Sekera47f35272023-03-29 16:04:58 +02001308 self.send_result_through_pipe(test, result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001309
1310 def addFailure(self, test, err):
1311 """
1312 Record a test failed result
1313
1314 :param test:
1315 :param err: error message
1316
1317 """
Klement Sekera47f35272023-03-29 16:04:58 +02001318 self.add_error(test, err, unittest.TestResult.addFailure, TestResultCode.FAIL)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001319
1320 def addError(self, test, err):
1321 """
1322 Record a test error result
1323
1324 :param test:
1325 :param err: error message
1326
1327 """
Klement Sekera47f35272023-03-29 16:04:58 +02001328 self.add_error(test, err, unittest.TestResult.addError, TestResultCode.ERROR)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001329
1330 def getDescription(self, test):
1331 """
1332 Get test description
1333
1334 :param test:
1335 :returns: test description
1336
1337 """
1338 return get_test_description(self.descriptions, test)
1339
1340 def startTest(self, test):
1341 """
1342 Start a test
1343
1344 :param test:
1345
1346 """
1347
1348 def print_header(test):
1349 if test.__class__ in self.printed:
1350 return
1351
1352 test_doc = getdoc(test)
1353 if not test_doc:
1354 raise Exception("No doc string for test '%s'" % test.id())
1355
1356 test_title = test_doc.splitlines()[0].rstrip()
1357 test_title = colorize(test_title, GREEN)
1358 if test.is_tagged_run_solo():
1359 test_title = colorize(f"SOLO RUN: {test_title}", YELLOW)
1360
1361 # This block may overwrite the colorized title above,
1362 # but we want this to stand out and be fixed
1363 if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
1364 test_title = colorize(f"FIXME with VPP workers: {test_title}", RED)
1365
1366 if test.has_tag(TestCaseTag.FIXME_ASAN):
1367 test_title = colorize(f"FIXME with ASAN: {test_title}", RED)
1368 test.skip_fixme_asan()
1369
Andrew Yourtchenko9987d472024-07-02 18:02:09 +02001370 if (
1371 test.has_tag(TestCaseTag.FIXME_UBUNTU2204)
1372 and is_distro_ubuntu2204 == True
1373 ):
1374 test_title = colorize(f"FIXME with Ubuntu 22.04: {test_title}", RED)
1375 test.skip_fixme_ubuntu2204()
1376
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001377 if hasattr(test, "vpp_worker_count"):
1378 if test.vpp_worker_count == 0:
1379 test_title += " [main thread only]"
1380 elif test.vpp_worker_count == 1:
1381 test_title += " [1 worker thread]"
1382 else:
1383 test_title += f" [{test.vpp_worker_count} worker threads]"
1384
1385 if test.__class__.skipped_due_to_cpu_lack:
1386 test_title = colorize(
1387 f"{test_title} [skipped - not enough cpus, "
1388 f"required={test.__class__.get_cpus_required()}, "
1389 f"available={max_vpp_cpus}]",
1390 YELLOW,
1391 )
1392
1393 print(double_line_delim)
1394 print(test_title)
1395 print(double_line_delim)
1396 self.printed.append(test.__class__)
1397
1398 print_header(test)
1399 self.start_test = time.time()
1400 unittest.TestResult.startTest(self, test)
1401 if self.verbosity > 0:
1402 self.stream.writeln("Starting " + self.getDescription(test) + " ...")
1403 self.stream.writeln(single_line_delim)
1404
1405 def stopTest(self, test):
1406 """
1407 Called when the given test has been run
1408
1409 :param test:
1410
1411 """
1412 unittest.TestResult.stopTest(self, test)
1413
Klement Sekera47f35272023-03-29 16:04:58 +02001414 result_code_to_suffix = {
1415 TestResultCode.PASS: "",
1416 TestResultCode.FAIL: "",
1417 TestResultCode.ERROR: "",
1418 TestResultCode.SKIP: "",
1419 TestResultCode.TEST_RUN: "",
1420 TestResultCode.SKIP_CPU_SHORTAGE: "",
1421 TestResultCode.EXPECTED_FAIL: " [EXPECTED FAIL]",
1422 TestResultCode.UNEXPECTED_PASS: " [UNEXPECTED PASS]",
1423 }
1424
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001425 if self.verbosity > 0:
1426 self.stream.writeln(single_line_delim)
1427 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001428 "%-72s%s%s"
1429 % (
1430 self.getDescription(test),
1431 self.result_string,
1432 result_code_to_suffix[self.result_code],
1433 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001434 )
1435 self.stream.writeln(single_line_delim)
1436 else:
1437 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001438 "%-67s %4.2f %s%s"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001439 % (
1440 self.getDescription(test),
1441 time.time() - self.start_test,
1442 self.result_string,
Klement Sekera47f35272023-03-29 16:04:58 +02001443 result_code_to_suffix[self.result_code],
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001444 )
1445 )
1446
Klement Sekera47f35272023-03-29 16:04:58 +02001447 self.send_result_through_pipe(test, TestResultCode.TEST_RUN)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001448
1449 def printErrors(self):
1450 """
1451 Print errors from running the test case
1452 """
1453 if len(self.errors) > 0 or len(self.failures) > 0:
1454 self.stream.writeln()
1455 self.printErrorList("ERROR", self.errors)
1456 self.printErrorList("FAIL", self.failures)
1457
1458 # ^^ that is the last output from unittest before summary
1459 if not self.runner.print_summary:
1460 devnull = unittest.runner._WritelnDecorator(open(os.devnull, "w"))
1461 self.stream = devnull
1462 self.runner.stream = devnull
1463
1464 def printErrorList(self, flavour, errors):
1465 """
1466 Print error list to the output stream together with error type
1467 and test case description.
1468
1469 :param flavour: error type
1470 :param errors: iterable errors
1471
1472 """
1473 for test, err in errors:
1474 self.stream.writeln(double_line_delim)
1475 self.stream.writeln("%s: %s" % (flavour, self.getDescription(test)))
1476 self.stream.writeln(single_line_delim)
1477 self.stream.writeln("%s" % err)
1478
1479
1480class VppTestRunner(unittest.TextTestRunner):
1481 """
1482 A basic test runner implementation which prints results to standard error.
1483 """
1484
1485 @property
1486 def resultclass(self):
1487 """Class maintaining the results of the tests"""
1488 return VppTestResult
1489
1490 def __init__(
1491 self,
1492 keep_alive_pipe=None,
1493 descriptions=True,
1494 verbosity=1,
1495 result_pipe=None,
1496 failfast=False,
1497 buffer=False,
1498 resultclass=None,
1499 print_summary=True,
1500 **kwargs,
1501 ):
1502 # ignore stream setting here, use hard-coded stdout to be in sync
Dave Wallace8800f732023-08-31 00:47:44 -04001503 # with prints from VppAsfTestCase methods ...
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001504 super(VppTestRunner, self).__init__(
1505 sys.stdout, descriptions, verbosity, failfast, buffer, resultclass, **kwargs
1506 )
1507 KeepAliveReporter.pipe = keep_alive_pipe
1508
1509 self.orig_stream = self.stream
1510 self.resultclass.test_framework_result_pipe = result_pipe
1511
1512 self.print_summary = print_summary
1513
1514 def _makeResult(self):
1515 return self.resultclass(self.stream, self.descriptions, self.verbosity, self)
1516
1517 def run(self, test):
1518 """
1519 Run the tests
1520
1521 :param test:
1522
1523 """
1524 faulthandler.enable() # emit stack trace to stderr if killed by signal
1525
1526 result = super(VppTestRunner, self).run(test)
1527 if not self.print_summary:
1528 self.stream = self.orig_stream
1529 result.stream = self.orig_stream
1530 return result
1531
1532
1533class Worker(Thread):
1534 def __init__(self, executable_args, logger, env=None, *args, **kwargs):
1535 super(Worker, self).__init__(*args, **kwargs)
1536 self.logger = logger
1537 self.args = executable_args
1538 if hasattr(self, "testcase") and self.testcase.debug_all:
1539 if self.testcase.debug_gdbserver:
1540 self.args = [
1541 "/usr/bin/gdbserver",
1542 "localhost:{port}".format(port=self.testcase.gdbserver_port),
1543 ] + args
1544 elif self.testcase.debug_gdb and hasattr(self, "wait_for_gdb"):
1545 self.args.append(self.wait_for_gdb)
1546 self.app_bin = executable_args[0]
1547 self.app_name = os.path.basename(self.app_bin)
1548 if hasattr(self, "role"):
1549 self.app_name += " {role}".format(role=self.role)
1550 self.process = None
1551 self.result = None
1552 env = {} if env is None else env
1553 self.env = copy.deepcopy(env)
1554
1555 def wait_for_enter(self):
1556 if not hasattr(self, "testcase"):
1557 return
1558 if self.testcase.debug_all and self.testcase.debug_gdbserver:
1559 print()
1560 print(double_line_delim)
1561 print(
1562 "Spawned GDB Server for '{app}' with PID: {pid}".format(
1563 app=self.app_name, pid=self.process.pid
1564 )
1565 )
1566 elif self.testcase.debug_all and self.testcase.debug_gdb:
1567 print()
1568 print(double_line_delim)
1569 print(
1570 "Spawned '{app}' with PID: {pid}".format(
1571 app=self.app_name, pid=self.process.pid
1572 )
1573 )
1574 else:
1575 return
1576 print(single_line_delim)
1577 print("You can debug '{app}' using:".format(app=self.app_name))
1578 if self.testcase.debug_gdbserver:
1579 print(
1580 "sudo gdb "
1581 + self.app_bin
1582 + " -ex 'target remote localhost:{port}'".format(
1583 port=self.testcase.gdbserver_port
1584 )
1585 )
1586 print(
1587 "Now is the time to attach gdb by running the above "
1588 "command, set up breakpoints etc., then resume from "
1589 "within gdb by issuing the 'continue' command"
1590 )
1591 self.testcase.gdbserver_port += 1
1592 elif self.testcase.debug_gdb:
1593 print(
1594 "sudo gdb "
1595 + self.app_bin
1596 + " -ex 'attach {pid}'".format(pid=self.process.pid)
1597 )
1598 print(
1599 "Now is the time to attach gdb by running the above "
1600 "command and set up breakpoints etc., then resume from"
1601 " within gdb by issuing the 'continue' command"
1602 )
1603 print(single_line_delim)
1604 input("Press ENTER to continue running the testcase...")
1605
1606 def run(self):
1607 executable = self.args[0]
1608 if not os.path.exists(executable) or not os.access(
1609 executable, os.F_OK | os.X_OK
1610 ):
1611 # Exit code that means some system file did not exist,
1612 # could not be opened, or had some other kind of error.
1613 self.result = os.EX_OSFILE
1614 raise EnvironmentError(
1615 "executable '%s' is not found or executable." % executable
1616 )
1617 self.logger.debug(
1618 "Running executable '{app}': '{cmd}'".format(
1619 app=self.app_name, cmd=" ".join(self.args)
1620 )
1621 )
1622 env = os.environ.copy()
1623 env.update(self.env)
1624 env["CK_LOG_FILE_NAME"] = "-"
1625 self.process = subprocess.Popen(
1626 ["stdbuf", "-o0", "-e0"] + self.args,
1627 shell=False,
1628 env=env,
1629 preexec_fn=os.setpgrp,
1630 stdout=subprocess.PIPE,
1631 stderr=subprocess.PIPE,
1632 )
1633 self.wait_for_enter()
1634 out, err = self.process.communicate()
1635 self.logger.debug("Finished running `{app}'".format(app=self.app_name))
1636 self.logger.info("Return code is `%s'" % self.process.returncode)
1637 self.logger.info(single_line_delim)
1638 self.logger.info(
1639 "Executable `{app}' wrote to stdout:".format(app=self.app_name)
1640 )
1641 self.logger.info(single_line_delim)
1642 self.logger.info(out.decode("utf-8"))
1643 self.logger.info(single_line_delim)
1644 self.logger.info(
1645 "Executable `{app}' wrote to stderr:".format(app=self.app_name)
1646 )
1647 self.logger.info(single_line_delim)
1648 self.logger.info(err.decode("utf-8"))
1649 self.logger.info(single_line_delim)
1650 self.result = self.process.returncode
1651
1652
1653if __name__ == "__main__":
1654 pass