blob: 0228af8264204f4a9643b910cea9e10cff14b13f [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
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001164 def decodePcapFiles(self, test):
1165 if hasattr(test, "pg_interfaces") and len(test.pg_interfaces) > 0:
1166 testcase_dir = os.path.dirname(test.pg_interfaces[0].out_path)
1167 test.pg_interfaces[0].decode_pcap_files(
1168 testcase_dir, f"suite{test.__class__.__name__}"
1169 )
1170 test.pg_interfaces[0].decode_pcap_files(testcase_dir, test._testMethodName)
Dave Wallace8800f732023-08-31 00:47:44 -04001171
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001172 def addSuccess(self, test):
1173 """
1174 Record a test succeeded result
1175
1176 :param test:
1177
1178 """
Klement Sekera47f35272023-03-29 16:04:58 +02001179 self.log_result("addSuccess", test)
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001180 if "all" == config.decode_pcaps:
1181 self.decodePcapFiles(test)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001182 unittest.TestResult.addSuccess(self, test)
1183 self.result_string = colorize("OK", GREEN)
Klement Sekera47f35272023-03-29 16:04:58 +02001184 self.result_code = TestResultCode.PASS
1185 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001186
Klement Sekera47f35272023-03-29 16:04:58 +02001187 def addExpectedFailure(self, test, err):
1188 self.log_result("addExpectedFailure", test, err)
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001189 if "none" != config.decode_pcaps:
1190 self.decodePcapFiles(test)
Klement Sekera47f35272023-03-29 16:04:58 +02001191 super().addExpectedFailure(test, err)
1192 self.result_string = colorize("FAIL", GREEN)
1193 self.result_code = TestResultCode.EXPECTED_FAIL
1194 self.send_result_through_pipe(test, self.result_code)
1195
1196 def addUnexpectedSuccess(self, test):
1197 self.log_result("addUnexpectedSuccess", test)
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001198 if "none" != config.decode_pcaps:
1199 self.decodePcapFiles(test)
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}")
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001285
1286 if "none" != config.decode_pcaps:
1287 self.decodePcapFiles(test)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001288
1289 unittest_fn(self, test, err)
1290 if self.current_test_case_info:
1291 self.result_string = "%s [ temp dir used by test case: %s ]" % (
1292 error_type_str,
1293 self.current_test_case_info.tempdir,
1294 )
1295 self.symlink_failed()
1296 self.failed_test_cases_info.add(self.current_test_case_info)
1297 if is_core_present(self.current_test_case_info.tempdir):
1298 if not self.current_test_case_info.core_crash_test:
1299 if isinstance(test, unittest.suite._ErrorHolder):
1300 test_name = str(test)
1301 else:
1302 test_name = "'{!s}' ({!s})".format(
1303 get_testcase_doc_name(test), test.id()
1304 )
1305 self.current_test_case_info.core_crash_test = test_name
1306 self.core_crash_test_cases_info.add(self.current_test_case_info)
1307 else:
1308 self.result_string = "%s [no temp dir]" % error_type_str
1309
Klement Sekera47f35272023-03-29 16:04:58 +02001310 self.send_result_through_pipe(test, result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001311
1312 def addFailure(self, test, err):
1313 """
1314 Record a test failed result
1315
1316 :param test:
1317 :param err: error message
1318
1319 """
Klement Sekera47f35272023-03-29 16:04:58 +02001320 self.add_error(test, err, unittest.TestResult.addFailure, TestResultCode.FAIL)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001321
1322 def addError(self, test, err):
1323 """
1324 Record a test error result
1325
1326 :param test:
1327 :param err: error message
1328
1329 """
Klement Sekera47f35272023-03-29 16:04:58 +02001330 self.add_error(test, err, unittest.TestResult.addError, TestResultCode.ERROR)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001331
1332 def getDescription(self, test):
1333 """
1334 Get test description
1335
1336 :param test:
1337 :returns: test description
1338
1339 """
1340 return get_test_description(self.descriptions, test)
1341
1342 def startTest(self, test):
1343 """
1344 Start a test
1345
1346 :param test:
1347
1348 """
1349
1350 def print_header(test):
1351 if test.__class__ in self.printed:
1352 return
1353
1354 test_doc = getdoc(test)
1355 if not test_doc:
1356 raise Exception("No doc string for test '%s'" % test.id())
1357
1358 test_title = test_doc.splitlines()[0].rstrip()
1359 test_title = colorize(test_title, GREEN)
1360 if test.is_tagged_run_solo():
1361 test_title = colorize(f"SOLO RUN: {test_title}", YELLOW)
1362
1363 # This block may overwrite the colorized title above,
1364 # but we want this to stand out and be fixed
1365 if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
1366 test_title = colorize(f"FIXME with VPP workers: {test_title}", RED)
1367
1368 if test.has_tag(TestCaseTag.FIXME_ASAN):
1369 test_title = colorize(f"FIXME with ASAN: {test_title}", RED)
1370 test.skip_fixme_asan()
1371
Andrew Yourtchenko9987d472024-07-02 18:02:09 +02001372 if (
1373 test.has_tag(TestCaseTag.FIXME_UBUNTU2204)
1374 and is_distro_ubuntu2204 == True
1375 ):
1376 test_title = colorize(f"FIXME with Ubuntu 22.04: {test_title}", RED)
1377 test.skip_fixme_ubuntu2204()
1378
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001379 if hasattr(test, "vpp_worker_count"):
1380 if test.vpp_worker_count == 0:
1381 test_title += " [main thread only]"
1382 elif test.vpp_worker_count == 1:
1383 test_title += " [1 worker thread]"
1384 else:
1385 test_title += f" [{test.vpp_worker_count} worker threads]"
1386
1387 if test.__class__.skipped_due_to_cpu_lack:
1388 test_title = colorize(
1389 f"{test_title} [skipped - not enough cpus, "
1390 f"required={test.__class__.get_cpus_required()}, "
1391 f"available={max_vpp_cpus}]",
1392 YELLOW,
1393 )
1394
1395 print(double_line_delim)
1396 print(test_title)
1397 print(double_line_delim)
1398 self.printed.append(test.__class__)
1399
1400 print_header(test)
1401 self.start_test = time.time()
1402 unittest.TestResult.startTest(self, test)
1403 if self.verbosity > 0:
1404 self.stream.writeln("Starting " + self.getDescription(test) + " ...")
1405 self.stream.writeln(single_line_delim)
1406
1407 def stopTest(self, test):
1408 """
1409 Called when the given test has been run
1410
1411 :param test:
1412
1413 """
1414 unittest.TestResult.stopTest(self, test)
1415
Klement Sekera47f35272023-03-29 16:04:58 +02001416 result_code_to_suffix = {
1417 TestResultCode.PASS: "",
1418 TestResultCode.FAIL: "",
1419 TestResultCode.ERROR: "",
1420 TestResultCode.SKIP: "",
1421 TestResultCode.TEST_RUN: "",
1422 TestResultCode.SKIP_CPU_SHORTAGE: "",
1423 TestResultCode.EXPECTED_FAIL: " [EXPECTED FAIL]",
1424 TestResultCode.UNEXPECTED_PASS: " [UNEXPECTED PASS]",
1425 }
1426
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001427 if self.verbosity > 0:
1428 self.stream.writeln(single_line_delim)
1429 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001430 "%-72s%s%s"
1431 % (
1432 self.getDescription(test),
1433 self.result_string,
1434 result_code_to_suffix[self.result_code],
1435 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001436 )
1437 self.stream.writeln(single_line_delim)
1438 else:
1439 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001440 "%-67s %4.2f %s%s"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001441 % (
1442 self.getDescription(test),
1443 time.time() - self.start_test,
1444 self.result_string,
Klement Sekera47f35272023-03-29 16:04:58 +02001445 result_code_to_suffix[self.result_code],
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001446 )
1447 )
1448
Klement Sekera47f35272023-03-29 16:04:58 +02001449 self.send_result_through_pipe(test, TestResultCode.TEST_RUN)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001450
1451 def printErrors(self):
1452 """
1453 Print errors from running the test case
1454 """
1455 if len(self.errors) > 0 or len(self.failures) > 0:
1456 self.stream.writeln()
1457 self.printErrorList("ERROR", self.errors)
1458 self.printErrorList("FAIL", self.failures)
1459
1460 # ^^ that is the last output from unittest before summary
1461 if not self.runner.print_summary:
1462 devnull = unittest.runner._WritelnDecorator(open(os.devnull, "w"))
1463 self.stream = devnull
1464 self.runner.stream = devnull
1465
1466 def printErrorList(self, flavour, errors):
1467 """
1468 Print error list to the output stream together with error type
1469 and test case description.
1470
1471 :param flavour: error type
1472 :param errors: iterable errors
1473
1474 """
1475 for test, err in errors:
1476 self.stream.writeln(double_line_delim)
1477 self.stream.writeln("%s: %s" % (flavour, self.getDescription(test)))
1478 self.stream.writeln(single_line_delim)
1479 self.stream.writeln("%s" % err)
1480
1481
1482class VppTestRunner(unittest.TextTestRunner):
1483 """
1484 A basic test runner implementation which prints results to standard error.
1485 """
1486
1487 @property
1488 def resultclass(self):
1489 """Class maintaining the results of the tests"""
1490 return VppTestResult
1491
1492 def __init__(
1493 self,
1494 keep_alive_pipe=None,
1495 descriptions=True,
1496 verbosity=1,
1497 result_pipe=None,
1498 failfast=False,
1499 buffer=False,
1500 resultclass=None,
1501 print_summary=True,
1502 **kwargs,
1503 ):
1504 # ignore stream setting here, use hard-coded stdout to be in sync
Dave Wallace8800f732023-08-31 00:47:44 -04001505 # with prints from VppAsfTestCase methods ...
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001506 super(VppTestRunner, self).__init__(
1507 sys.stdout, descriptions, verbosity, failfast, buffer, resultclass, **kwargs
1508 )
1509 KeepAliveReporter.pipe = keep_alive_pipe
1510
1511 self.orig_stream = self.stream
1512 self.resultclass.test_framework_result_pipe = result_pipe
1513
1514 self.print_summary = print_summary
1515
1516 def _makeResult(self):
1517 return self.resultclass(self.stream, self.descriptions, self.verbosity, self)
1518
1519 def run(self, test):
1520 """
1521 Run the tests
1522
1523 :param test:
1524
1525 """
1526 faulthandler.enable() # emit stack trace to stderr if killed by signal
1527
1528 result = super(VppTestRunner, self).run(test)
1529 if not self.print_summary:
1530 self.stream = self.orig_stream
1531 result.stream = self.orig_stream
1532 return result
1533
1534
1535class Worker(Thread):
1536 def __init__(self, executable_args, logger, env=None, *args, **kwargs):
1537 super(Worker, self).__init__(*args, **kwargs)
1538 self.logger = logger
1539 self.args = executable_args
1540 if hasattr(self, "testcase") and self.testcase.debug_all:
1541 if self.testcase.debug_gdbserver:
1542 self.args = [
1543 "/usr/bin/gdbserver",
1544 "localhost:{port}".format(port=self.testcase.gdbserver_port),
1545 ] + args
1546 elif self.testcase.debug_gdb and hasattr(self, "wait_for_gdb"):
1547 self.args.append(self.wait_for_gdb)
1548 self.app_bin = executable_args[0]
1549 self.app_name = os.path.basename(self.app_bin)
1550 if hasattr(self, "role"):
1551 self.app_name += " {role}".format(role=self.role)
1552 self.process = None
1553 self.result = None
1554 env = {} if env is None else env
1555 self.env = copy.deepcopy(env)
1556
1557 def wait_for_enter(self):
1558 if not hasattr(self, "testcase"):
1559 return
1560 if self.testcase.debug_all and self.testcase.debug_gdbserver:
1561 print()
1562 print(double_line_delim)
1563 print(
1564 "Spawned GDB Server for '{app}' with PID: {pid}".format(
1565 app=self.app_name, pid=self.process.pid
1566 )
1567 )
1568 elif self.testcase.debug_all and self.testcase.debug_gdb:
1569 print()
1570 print(double_line_delim)
1571 print(
1572 "Spawned '{app}' with PID: {pid}".format(
1573 app=self.app_name, pid=self.process.pid
1574 )
1575 )
1576 else:
1577 return
1578 print(single_line_delim)
1579 print("You can debug '{app}' using:".format(app=self.app_name))
1580 if self.testcase.debug_gdbserver:
1581 print(
1582 "sudo gdb "
1583 + self.app_bin
1584 + " -ex 'target remote localhost:{port}'".format(
1585 port=self.testcase.gdbserver_port
1586 )
1587 )
1588 print(
1589 "Now is the time to attach gdb by running the above "
1590 "command, set up breakpoints etc., then resume from "
1591 "within gdb by issuing the 'continue' command"
1592 )
1593 self.testcase.gdbserver_port += 1
1594 elif self.testcase.debug_gdb:
1595 print(
1596 "sudo gdb "
1597 + self.app_bin
1598 + " -ex 'attach {pid}'".format(pid=self.process.pid)
1599 )
1600 print(
1601 "Now is the time to attach gdb by running the above "
1602 "command and set up breakpoints etc., then resume from"
1603 " within gdb by issuing the 'continue' command"
1604 )
1605 print(single_line_delim)
1606 input("Press ENTER to continue running the testcase...")
1607
1608 def run(self):
1609 executable = self.args[0]
1610 if not os.path.exists(executable) or not os.access(
1611 executable, os.F_OK | os.X_OK
1612 ):
1613 # Exit code that means some system file did not exist,
1614 # could not be opened, or had some other kind of error.
1615 self.result = os.EX_OSFILE
1616 raise EnvironmentError(
1617 "executable '%s' is not found or executable." % executable
1618 )
1619 self.logger.debug(
1620 "Running executable '{app}': '{cmd}'".format(
1621 app=self.app_name, cmd=" ".join(self.args)
1622 )
1623 )
1624 env = os.environ.copy()
1625 env.update(self.env)
1626 env["CK_LOG_FILE_NAME"] = "-"
1627 self.process = subprocess.Popen(
1628 ["stdbuf", "-o0", "-e0"] + self.args,
1629 shell=False,
1630 env=env,
1631 preexec_fn=os.setpgrp,
1632 stdout=subprocess.PIPE,
1633 stderr=subprocess.PIPE,
1634 )
1635 self.wait_for_enter()
1636 out, err = self.process.communicate()
1637 self.logger.debug("Finished running `{app}'".format(app=self.app_name))
1638 self.logger.info("Return code is `%s'" % self.process.returncode)
1639 self.logger.info(single_line_delim)
1640 self.logger.info(
1641 "Executable `{app}' wrote to stdout:".format(app=self.app_name)
1642 )
1643 self.logger.info(single_line_delim)
1644 self.logger.info(out.decode("utf-8"))
1645 self.logger.info(single_line_delim)
1646 self.logger.info(
1647 "Executable `{app}' wrote to stderr:".format(app=self.app_name)
1648 )
1649 self.logger.info(single_line_delim)
1650 self.logger.info(err.decode("utf-8"))
1651 self.logger.info(single_line_delim)
1652 self.result = self.process.returncode
1653
1654
1655if __name__ == "__main__":
1656 pass