blob: 9839ff585434784409d57791113b3cba0d244e18 [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
320 def instance(cls):
321 """Return the instance of this testcase"""
322 return cls.test_instance
323
324 @classmethod
325 def set_debug_flags(cls, d):
326 cls.gdbserver_port = 7777
327 cls.debug_core = False
328 cls.debug_gdb = False
329 cls.debug_gdbserver = False
330 cls.debug_all = False
331 cls.debug_attach = False
332 if d is None:
333 return
334 dl = d.lower()
335 if dl == "core":
336 cls.debug_core = True
337 elif dl == "gdb" or dl == "gdb-all":
338 cls.debug_gdb = True
339 elif dl == "gdbserver" or dl == "gdbserver-all":
340 cls.debug_gdbserver = True
341 elif dl == "attach":
342 cls.debug_attach = True
343 else:
344 raise Exception("Unrecognized DEBUG option: '%s'" % d)
345 if dl == "gdb-all" or dl == "gdbserver-all":
346 cls.debug_all = True
347
348 @classmethod
349 def get_vpp_worker_count(cls):
350 if not hasattr(cls, "vpp_worker_count"):
351 if cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
352 cls.vpp_worker_count = 0
353 else:
354 cls.vpp_worker_count = config.vpp_worker_count
355 return cls.vpp_worker_count
356
357 @classmethod
358 def get_cpus_required(cls):
359 return 1 + cls.get_vpp_worker_count()
360
361 @classmethod
362 def setUpConstants(cls):
363 """Set-up the test case class based on environment variables"""
364 cls.step = config.step
365 cls.plugin_path = ":".join(config.vpp_plugin_dir)
366 cls.test_plugin_path = ":".join(config.vpp_test_plugin_dir)
367 cls.extern_plugin_path = ":".join(config.extern_plugin_dir)
368 debug_cli = ""
369 if cls.step or cls.debug_gdb or cls.debug_gdbserver:
370 debug_cli = "cli-listen localhost:5002"
371 size = re.search(r"\d+[gG]", config.coredump_size)
372 if size:
373 coredump_size = f"coredump-size {config.coredump_size}".lower()
374 else:
375 coredump_size = "coredump-size unlimited"
376 default_variant = config.variant
377 if default_variant is not None:
378 default_variant = "defaults { %s 100 }" % default_variant
379 else:
380 default_variant = ""
381
382 api_fuzzing = config.api_fuzz
383 if api_fuzzing is None:
384 api_fuzzing = "off"
385
386 cls.vpp_cmdline = [
387 config.vpp,
388 "unix",
389 "{",
390 "nodaemon",
391 debug_cli,
392 "full-coredump",
393 coredump_size,
394 "runtime-dir",
395 cls.tempdir,
396 "}",
397 "api-trace",
398 "{",
399 "on",
400 "}",
401 "api-segment",
402 "{",
403 "prefix",
404 cls.get_api_segment_prefix(),
405 "}",
406 "cpu",
407 "{",
408 "main-core",
409 str(cls.cpus[0]),
410 ]
411 if cls.extern_plugin_path not in (None, ""):
412 cls.extra_vpp_plugin_config.append("add-path %s" % cls.extern_plugin_path)
413 if cls.get_vpp_worker_count():
414 cls.vpp_cmdline.extend(
415 ["corelist-workers", ",".join([str(x) for x in cls.cpus[1:]])]
416 )
417 cls.vpp_cmdline.extend(
418 [
419 "}",
420 "physmem",
421 "{",
422 "max-size",
423 "32m",
424 "}",
425 "statseg",
426 "{",
427 "socket-name",
428 cls.get_stats_sock_path(),
429 cls.extra_vpp_statseg_config,
430 "}",
431 "socksvr",
432 "{",
433 "socket-name",
434 cls.get_api_sock_path(),
435 "}",
436 "node { ",
437 default_variant,
438 "}",
439 "api-fuzz {",
440 api_fuzzing,
441 "}",
442 "plugins",
443 "{",
444 "plugin",
445 "dpdk_plugin.so",
446 "{",
447 "disable",
448 "}",
449 "plugin",
450 "rdma_plugin.so",
451 "{",
452 "disable",
453 "}",
454 "plugin",
455 "lisp_unittest_plugin.so",
456 "{",
457 "enable",
458 "}",
459 "plugin",
460 "unittest_plugin.so",
461 "{",
462 "enable",
463 "}",
464 ]
465 + cls.extra_vpp_plugin_config
466 + [
467 "}",
468 ]
469 )
470
Klement Sekerad3e0d102023-01-26 12:35:35 +0100471 if cls.extra_vpp_config is not None:
472 cls.vpp_cmdline.extend(cls.extra_vpp_config)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400473
474 if not cls.debug_attach:
475 cls.logger.info("vpp_cmdline args: %s" % cls.vpp_cmdline)
476 cls.logger.info("vpp_cmdline: %s" % " ".join(cls.vpp_cmdline))
477
478 @classmethod
479 def wait_for_enter(cls):
480 if cls.debug_gdbserver:
481 print(double_line_delim)
482 print("Spawned GDB server with PID: %d" % cls.vpp.pid)
483 elif cls.debug_gdb:
484 print(double_line_delim)
485 print("Spawned VPP with PID: %d" % cls.vpp.pid)
486 else:
487 cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid)
488 return
489 print(single_line_delim)
490 print("You can debug VPP using:")
491 if cls.debug_gdbserver:
492 print(
493 f"sudo gdb {config.vpp} "
494 f"-ex 'target remote localhost:{cls.gdbserver_port}'"
495 )
496 print(
497 "Now is the time to attach gdb by running the above "
498 "command, set up breakpoints etc., then resume VPP from "
499 "within gdb by issuing the 'continue' command"
500 )
501 cls.gdbserver_port += 1
502 elif cls.debug_gdb:
503 print(f"sudo gdb {config.vpp} -ex 'attach {cls.vpp.pid}'")
504 print(
505 "Now is the time to attach gdb by running the above "
506 "command and set up breakpoints etc., then resume VPP from"
507 " within gdb by issuing the 'continue' command"
508 )
509 print(single_line_delim)
510 input("Press ENTER to continue running the testcase...")
511
512 @classmethod
513 def attach_vpp(cls):
514 cls.vpp = DummyVpp()
515
516 @classmethod
517 def run_vpp(cls):
518 cls.logger.debug(f"Assigned cpus: {cls.cpus}")
519 cmdline = cls.vpp_cmdline
520
521 if cls.debug_gdbserver:
522 gdbserver = "/usr/bin/gdbserver"
523 if not os.path.isfile(gdbserver) or not os.access(gdbserver, os.X_OK):
524 raise Exception(
525 "gdbserver binary '%s' does not exist or is "
526 "not executable" % gdbserver
527 )
528
529 cmdline = [
530 gdbserver,
531 "localhost:{port}".format(port=cls.gdbserver_port),
532 ] + cls.vpp_cmdline
533 cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline))
534
535 try:
536 cls.vpp = subprocess.Popen(
537 cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE
538 )
539 except subprocess.CalledProcessError as e:
540 cls.logger.critical(
541 "Subprocess returned with non-0 return code: (%s)", e.returncode
542 )
543 raise
544 except OSError as e:
545 cls.logger.critical(
546 "Subprocess returned with OS error: (%s) %s", e.errno, e.strerror
547 )
548 raise
549 except Exception as e:
550 cls.logger.exception("Subprocess returned unexpected from %s:", cmdline)
551 raise
552
553 cls.wait_for_enter()
554
555 @classmethod
556 def wait_for_coredump(cls):
557 corefile = cls.tempdir + "/core"
558 if os.path.isfile(corefile):
559 cls.logger.error("Waiting for coredump to complete: %s", corefile)
560 curr_size = os.path.getsize(corefile)
561 deadline = time.time() + 60
562 ok = False
563 while time.time() < deadline:
564 cls.sleep(1)
565 size = curr_size
566 curr_size = os.path.getsize(corefile)
567 if size == curr_size:
568 ok = True
569 break
570 if not ok:
571 cls.logger.error(
572 "Timed out waiting for coredump to complete: %s", corefile
573 )
574 else:
575 cls.logger.error("Coredump complete: %s, size %d", corefile, curr_size)
576
577 @classmethod
578 def get_stats_sock_path(cls):
579 return "%s/stats.sock" % cls.tempdir
580
581 @classmethod
582 def get_api_sock_path(cls):
583 return "%s/api.sock" % cls.tempdir
584
585 @classmethod
586 def get_api_segment_prefix(cls):
587 return os.path.basename(cls.tempdir) # Only used for VAPI
588
589 @classmethod
590 def get_tempdir(cls):
591 if cls.debug_attach:
592 tmpdir = f"{config.tmp_dir}/unittest-attach-gdb"
593 else:
Dave Wallace8800f732023-08-31 00:47:44 -0400594 tmpdir = f"{config.tmp_dir}/{get_testcase_dirname(cls.__name__)}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400595 if config.wipe_tmp_dir:
596 shutil.rmtree(tmpdir, ignore_errors=True)
597 os.mkdir(tmpdir)
598 return tmpdir
599
600 @classmethod
601 def create_file_handler(cls):
602 if config.log_dir is None:
603 cls.file_handler = FileHandler(f"{cls.tempdir}/log.txt")
604 return
605
Dave Wallace8800f732023-08-31 00:47:44 -0400606 logdir = f"{config.log_dir}/{get_testcase_dirname(cls.__name__)}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400607 if config.wipe_tmp_dir:
608 shutil.rmtree(logdir, ignore_errors=True)
609 os.mkdir(logdir)
610 cls.file_handler = FileHandler(f"{logdir}/log.txt")
611
612 @classmethod
613 def setUpClass(cls):
614 """
615 Perform class setup before running the testcase
616 Remove shared memory files, start vpp and connect the vpp-api
617 """
Dave Wallace8800f732023-08-31 00:47:44 -0400618 super(VppAsfTestCase, cls).setUpClass()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400619 cls.logger = get_logger(cls.__name__)
Dave Wallace8800f732023-08-31 00:47:44 -0400620 cls.logger.debug(f"--- START setUpClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400621 random.seed(config.rnd_seed)
622 if hasattr(cls, "parallel_handler"):
623 cls.logger.addHandler(cls.parallel_handler)
624 cls.logger.propagate = False
625 cls.set_debug_flags(config.debug)
626 cls.tempdir = cls.get_tempdir()
627 cls.create_file_handler()
628 cls.file_handler.setFormatter(
629 Formatter(fmt="%(asctime)s,%(msecs)03d %(message)s", datefmt="%H:%M:%S")
630 )
631 cls.file_handler.setLevel(DEBUG)
632 cls.logger.addHandler(cls.file_handler)
633 cls.logger.debug("--- setUpClass() for %s called ---" % cls.__name__)
634 os.chdir(cls.tempdir)
635 cls.logger.info(
636 "Temporary dir is %s, api socket is %s",
637 cls.tempdir,
638 cls.get_api_sock_path(),
639 )
640 cls.logger.debug("Random seed is %s", config.rnd_seed)
641 cls.setUpConstants()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400642 cls.verbose = 0
643 cls.vpp_dead = False
644 cls.registry = VppObjectRegistry()
645 cls.vpp_startup_failed = False
646 cls.reporter = KeepAliveReporter()
647 # need to catch exceptions here because if we raise, then the cleanup
648 # doesn't get called and we might end with a zombie vpp
649 try:
650 if cls.debug_attach:
651 cls.attach_vpp()
652 else:
653 cls.run_vpp()
654 cls.reporter.send_keep_alive(cls, "setUpClass")
655 VppTestResult.current_test_case_info = TestCaseInfo(
656 cls.logger, cls.tempdir, cls.vpp.pid, config.vpp
657 )
658 cls.vpp_stdout_deque = deque()
659 cls.vpp_stderr_deque = deque()
660 if not cls.debug_attach:
661 cls.pump_thread_stop_flag = Event()
662 cls.pump_thread_wakeup_pipe = os.pipe()
663 cls.pump_thread = Thread(target=pump_output, args=(cls,))
664 cls.pump_thread.daemon = True
665 cls.pump_thread.start()
666 if cls.debug_gdb or cls.debug_gdbserver or cls.debug_attach:
667 cls.vapi_response_timeout = 0
668 cls.vapi = VppPapiProvider(cls.__name__, cls, cls.vapi_response_timeout)
669 if cls.step:
670 hook = hookmodule.StepHook(cls)
671 else:
672 hook = hookmodule.PollHook(cls)
673 cls.vapi.register_hook(hook)
674 cls.statistics = VPPStats(socketname=cls.get_stats_sock_path())
675 try:
676 hook.poll_vpp()
677 except VppDiedError:
Dave Wallace8800f732023-08-31 00:47:44 -0400678 cls.wait_for_coredump()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400679 cls.vpp_startup_failed = True
680 cls.logger.critical(
681 "VPP died shortly after startup, check the"
682 " output to standard error for possible cause"
683 )
684 raise
685 try:
686 cls.vapi.connect()
687 except (vpp_papi.VPPIOError, Exception) as e:
688 cls.logger.debug("Exception connecting to vapi: %s" % e)
689 cls.vapi.disconnect()
690
691 if cls.debug_gdbserver:
692 print(
693 colorize(
694 "You're running VPP inside gdbserver but "
695 "VPP-API connection failed, did you forget "
696 "to 'continue' VPP from within gdb?",
697 RED,
698 )
699 )
700 raise e
701 if cls.debug_attach:
702 last_line = cls.vapi.cli("show thread").split("\n")[-2]
703 cls.vpp_worker_count = int(last_line.split(" ")[0])
704 print("Detected VPP with %s workers." % cls.vpp_worker_count)
705 except vpp_papi.VPPRuntimeError as e:
706 cls.logger.debug("%s" % e)
707 cls.quit()
708 raise e
709 except Exception as e:
710 cls.logger.debug("Exception connecting to VPP: %s" % e)
711 cls.quit()
712 raise e
Dave Wallace8800f732023-08-31 00:47:44 -0400713 cls.logger.debug(f"--- END setUpClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400714
715 @classmethod
716 def _debug_quit(cls):
717 if cls.debug_gdbserver or cls.debug_gdb:
718 try:
719 cls.vpp.poll()
720
721 if cls.vpp.returncode is None:
722 print()
723 print(double_line_delim)
724 print("VPP or GDB server is still running")
725 print(single_line_delim)
726 input(
727 "When done debugging, press ENTER to kill the "
728 "process and finish running the testcase..."
729 )
730 except AttributeError:
731 pass
732
733 @classmethod
734 def quit(cls):
735 """
736 Disconnect vpp-api, kill vpp and cleanup shared memory files
737 """
738 cls._debug_quit()
739
740 # first signal that we want to stop the pump thread, then wake it up
741 if hasattr(cls, "pump_thread_stop_flag"):
742 cls.pump_thread_stop_flag.set()
743 if hasattr(cls, "pump_thread_wakeup_pipe"):
744 os.write(cls.pump_thread_wakeup_pipe[1], b"ding dong wake up")
745 if hasattr(cls, "pump_thread"):
746 cls.logger.debug("Waiting for pump thread to stop")
747 cls.pump_thread.join()
748 if hasattr(cls, "vpp_stderr_reader_thread"):
749 cls.logger.debug("Waiting for stderr pump to stop")
750 cls.vpp_stderr_reader_thread.join()
751
752 if hasattr(cls, "vpp"):
753 if hasattr(cls, "vapi"):
754 cls.logger.debug(cls.vapi.vpp.get_stats())
755 cls.logger.debug("Disconnecting class vapi client on %s", cls.__name__)
756 cls.vapi.disconnect()
757 cls.logger.debug("Deleting class vapi attribute on %s", cls.__name__)
758 del cls.vapi
759 cls.vpp.poll()
760 if not cls.debug_attach and cls.vpp.returncode is None:
761 cls.wait_for_coredump()
762 cls.logger.debug("Sending TERM to vpp")
763 cls.vpp.terminate()
764 cls.logger.debug("Waiting for vpp to die")
765 try:
766 outs, errs = cls.vpp.communicate(timeout=5)
767 except subprocess.TimeoutExpired:
768 cls.vpp.kill()
769 outs, errs = cls.vpp.communicate()
770 cls.logger.debug("Deleting class vpp attribute on %s", cls.__name__)
771 if not cls.debug_attach:
772 cls.vpp.stdout.close()
773 cls.vpp.stderr.close()
774 del cls.vpp
775
776 if cls.vpp_startup_failed:
777 stdout_log = cls.logger.info
778 stderr_log = cls.logger.critical
779 else:
780 stdout_log = cls.logger.info
781 stderr_log = cls.logger.info
782
783 if hasattr(cls, "vpp_stdout_deque"):
784 stdout_log(single_line_delim)
785 stdout_log("VPP output to stdout while running %s:", cls.__name__)
786 stdout_log(single_line_delim)
787 vpp_output = "".join(cls.vpp_stdout_deque)
788 with open(cls.tempdir + "/vpp_stdout.txt", "w") as f:
789 f.write(vpp_output)
790 stdout_log("\n%s", vpp_output)
791 stdout_log(single_line_delim)
792
793 if hasattr(cls, "vpp_stderr_deque"):
794 stderr_log(single_line_delim)
795 stderr_log("VPP output to stderr while running %s:", cls.__name__)
796 stderr_log(single_line_delim)
797 vpp_output = "".join(cls.vpp_stderr_deque)
798 with open(cls.tempdir + "/vpp_stderr.txt", "w") as f:
799 f.write(vpp_output)
800 stderr_log("\n%s", vpp_output)
801 stderr_log(single_line_delim)
802
803 @classmethod
804 def tearDownClass(cls):
805 """Perform final cleanup after running all tests in this test-case"""
Dave Wallace8800f732023-08-31 00:47:44 -0400806 cls.logger.debug(f"--- START tearDownClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400807 cls.reporter.send_keep_alive(cls, "tearDownClass")
808 cls.quit()
809 cls.file_handler.close()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400810 if config.debug_framework:
811 debug_internal.on_tear_down_class(cls)
Dave Wallace8800f732023-08-31 00:47:44 -0400812 cls.logger.debug(f"--- END tearDownClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400813
814 def show_commands_at_teardown(self):
815 """Allow subclass specific teardown logging additions."""
816 self.logger.info("--- No test specific show commands provided. ---")
817
818 def tearDown(self):
819 """Show various debug prints after each test"""
820 self.logger.debug(
Dave Wallace8800f732023-08-31 00:47:44 -0400821 f"--- START tearDown() {self.__class__.__name__}.{self._testMethodName}({self._testMethodDoc}) ---"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400822 )
823
824 try:
825 if not self.vpp_dead:
826 self.logger.debug(self.vapi.cli("show trace max 1000"))
827 self.logger.info(self.vapi.ppcli("show interface"))
828 self.logger.info(self.vapi.ppcli("show hardware"))
829 self.logger.info(self.statistics.set_errors_str())
830 self.logger.info(self.vapi.ppcli("show run"))
831 self.logger.info(self.vapi.ppcli("show log"))
832 self.logger.info(self.vapi.ppcli("show bihash"))
833 self.logger.info("Logging testcase specific show commands.")
834 self.show_commands_at_teardown()
835 if self.remove_configured_vpp_objects_on_tear_down:
836 self.registry.remove_vpp_config(self.logger)
837 # Save/Dump VPP api trace log
838 m = self._testMethodName
839 api_trace = "vpp_api_trace.%s.%d.log" % (m, self.vpp.pid)
840 tmp_api_trace = "/tmp/%s" % api_trace
841 vpp_api_trace_log = "%s/%s" % (self.tempdir, api_trace)
842 self.logger.info(self.vapi.ppcli("api trace save %s" % api_trace))
843 self.logger.info("Moving %s to %s\n" % (tmp_api_trace, vpp_api_trace_log))
Dmitry Valter71d02aa2023-01-27 12:49:55 +0000844 shutil.move(tmp_api_trace, vpp_api_trace_log)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400845 except VppTransportSocketIOError:
846 self.logger.debug(
847 "VppTransportSocketIOError: Vpp dead. Cannot log show commands."
848 )
849 self.vpp_dead = True
850 else:
851 self.registry.unregister_all(self.logger)
Dave Wallace8800f732023-08-31 00:47:44 -0400852 # Remove any leftover pcap files
853 if hasattr(self, "pg_interfaces") and len(self.pg_interfaces) > 0:
854 testcase_dir = os.path.dirname(self.pg_interfaces[0].out_path)
855 for p in Path(testcase_dir).glob("pg*.pcap"):
Dave Wallace1b86f0f2023-11-08 15:55:56 -0500856 retries = 8
857 while retries > 0:
858 retries = retries - 1
859 self.logger.debug(f"Unlinking {p}")
860 try:
861 p.unlink()
862 break
863 except OSError:
864 self.logger.debug(f"OSError: unlinking {p}")
865 self.sleep(0.25, f"{retries} retries left")
866
Dave Wallace8800f732023-08-31 00:47:44 -0400867 self.logger.debug(
868 f"--- END tearDown() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
869 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400870
871 def setUp(self):
872 """Clear trace before running each test"""
Dave Wallace8800f732023-08-31 00:47:44 -0400873 super(VppAsfTestCase, self).setUp()
874 self.logger.debug(
875 f"--- START setUp() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
876 )
877 # Save testname include in pcap history filenames
878 if hasattr(self, "pg_interfaces"):
879 for i in self.pg_interfaces:
880 i.test_name = self._testMethodName
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400881 self.reporter.send_keep_alive(self)
882 if self.vpp_dead:
Dave Wallace8800f732023-08-31 00:47:44 -0400883 self.wait_for_coredump()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400884 raise VppDiedError(
885 rv=None,
886 testcase=self.__class__.__name__,
887 method_name=self._testMethodName,
888 )
889 self.sleep(0.1, "during setUp")
890 self.vpp_stdout_deque.append(
891 "--- test setUp() for %s.%s(%s) starts here ---\n"
892 % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)
893 )
894 self.vpp_stderr_deque.append(
895 "--- test setUp() for %s.%s(%s) starts here ---\n"
896 % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)
897 )
898 self.vapi.cli("clear trace")
899 # store the test instance inside the test class - so that objects
900 # holding the class can access instance methods (like assertEqual)
901 type(self).test_instance = self
Dave Wallace8800f732023-08-31 00:47:44 -0400902 self.logger.debug(
903 f"--- END setUp() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
904 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400905
906 @classmethod
907 def get_vpp_time(cls):
908 # processes e.g. "Time now 2.190522, Wed, 11 Mar 2020 17:29:54 GMT"
909 # returns float("2.190522")
910 timestr = cls.vapi.cli("show clock")
911 head, sep, tail = timestr.partition(",")
912 head, sep, tail = head.partition("Time now")
913 return float(tail)
914
915 @classmethod
916 def sleep_on_vpp_time(cls, sec):
917 """Sleep according to time in VPP world"""
918 # On a busy system with many processes
919 # we might end up with VPP time being slower than real world
920 # So take that into account when waiting for VPP to do something
921 start_time = cls.get_vpp_time()
922 while cls.get_vpp_time() - start_time < sec:
923 cls.sleep(0.1)
924
925 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400926 def create_loopback_interfaces(cls, count):
927 """
928 Create loopback interfaces.
929
930 :param count: number of interfaces created.
931 :returns: List of created interfaces.
932 """
933 result = [VppLoInterface(cls) for i in range(count)]
934 for intf in result:
935 setattr(cls, intf.name, intf)
936 cls.lo_interfaces = result
937 return result
938
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400939 def assert_equal(self, real_value, expected_value, name_or_class=None):
940 if name_or_class is None:
941 self.assertEqual(real_value, expected_value)
942 return
943 try:
944 msg = "Invalid %s: %d('%s') does not match expected value %d('%s')"
945 msg = msg % (
946 getdoc(name_or_class).strip(),
947 real_value,
948 str(name_or_class(real_value)),
949 expected_value,
950 str(name_or_class(expected_value)),
951 )
952 except Exception:
953 msg = "Invalid %s: %s does not match expected value %s" % (
954 name_or_class,
955 real_value,
956 expected_value,
957 )
958
959 self.assertEqual(real_value, expected_value, msg)
960
961 def assert_in_range(self, real_value, expected_min, expected_max, name=None):
962 if name is None:
963 msg = None
964 else:
965 msg = "Invalid %s: %s out of range <%s,%s>" % (
966 name,
967 real_value,
968 expected_min,
969 expected_max,
970 )
971 self.assertTrue(expected_min <= real_value <= expected_max, msg)
972
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400973 def get_counter(self, counter):
974 if counter.startswith("/"):
975 counter_value = self.statistics.get_counter(counter)
976 else:
977 counters = self.vapi.cli("sh errors").split("\n")
978 counter_value = 0
979 for i in range(1, len(counters) - 1):
980 results = counters[i].split()
981 if results[1] == counter:
982 counter_value = int(results[0])
983 break
984 return counter_value
985
986 def assert_counter_equal(self, counter, expected_value, thread=None, index=0):
987 c = self.get_counter(counter)
988 if thread is not None:
989 c = c[thread][index]
990 else:
991 c = sum(x[index] for x in c)
Klement Sekerac5fa5392023-08-17 19:38:34 +0200992 self.logger.debug(
993 "validate counter `%s[%s]', expected: %s, real value: %s"
994 % (counter, index, expected_value, c)
995 )
996 self.assert_equal(c, expected_value, "counter `%s[%s]'" % (counter, index))
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400997
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400998 def assert_error_counter_equal(self, counter, expected_value):
999 counter_value = self.statistics[counter].sum()
1000 self.assert_equal(counter_value, expected_value, "error counter `%s'" % counter)
1001
1002 @classmethod
1003 def sleep(cls, timeout, remark=None):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001004 # /* Allow sleep(0) to maintain win32 semantics, and as decreed
1005 # * by Guido, only the main thread can be interrupted.
1006 # */
1007 # https://github.com/python/cpython/blob/6673decfa0fb078f60587f5cb5e98460eea137c2/Modules/timemodule.c#L1892 # noqa
1008 if timeout == 0:
1009 # yield quantum
1010 if hasattr(os, "sched_yield"):
1011 os.sched_yield()
1012 else:
1013 time.sleep(0)
1014 return
1015
1016 cls.logger.debug("Starting sleep for %es (%s)", timeout, remark)
1017 before = time.time()
1018 time.sleep(timeout)
1019 after = time.time()
1020 if after - before > 2 * timeout:
1021 cls.logger.error(
1022 "unexpected self.sleep() result - slept for %es instead of ~%es!",
1023 after - before,
1024 timeout,
1025 )
1026
1027 cls.logger.debug(
1028 "Finished sleep (%s) - slept %es (wanted %es)",
1029 remark,
1030 after - before,
1031 timeout,
1032 )
1033
1034 def virtual_sleep(self, timeout, remark=None):
1035 self.logger.debug("Moving VPP time by %s (%s)", timeout, remark)
1036 self.vapi.cli("set clock adjust %s" % timeout)
1037
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001038 def snapshot_stats(self, stats_diff):
1039 """Return snapshot of interesting stats based on diff dictionary."""
1040 stats_snapshot = {}
1041 for sw_if_index in stats_diff:
1042 for counter in stats_diff[sw_if_index]:
1043 stats_snapshot[counter] = self.statistics[counter]
1044 self.logger.debug(f"Took statistics stats_snapshot: {stats_snapshot}")
1045 return stats_snapshot
1046
1047 def compare_stats_with_snapshot(self, stats_diff, stats_snapshot):
1048 """Assert appropriate difference between current stats and snapshot."""
1049 for sw_if_index in stats_diff:
1050 for cntr, diff in stats_diff[sw_if_index].items():
1051 if sw_if_index == "err":
1052 self.assert_equal(
1053 self.statistics[cntr].sum(),
1054 stats_snapshot[cntr].sum() + diff,
1055 f"'{cntr}' counter value (previous value: "
1056 f"{stats_snapshot[cntr].sum()}, "
1057 f"expected diff: {diff})",
1058 )
1059 else:
1060 try:
1061 self.assert_equal(
1062 self.statistics[cntr][:, sw_if_index].sum(),
1063 stats_snapshot[cntr][:, sw_if_index].sum() + diff,
1064 f"'{cntr}' counter value (previous value: "
1065 f"{stats_snapshot[cntr][:, sw_if_index].sum()}, "
1066 f"expected diff: {diff})",
1067 )
Klement Sekera01578852023-01-26 13:14:01 +01001068 except IndexError as e:
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001069 # if diff is 0, then this most probably a case where
1070 # test declares multiple interfaces but traffic hasn't
1071 # passed through this one yet - which means the counter
1072 # value is 0 and can be ignored
1073 if 0 != diff:
Klement Sekera01578852023-01-26 13:14:01 +01001074 raise Exception(
1075 f"Couldn't sum counter: {cntr} on sw_if_index: {sw_if_index}"
1076 ) from e
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001077
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001078
1079def get_testcase_doc_name(test):
1080 return getdoc(test.__class__).splitlines()[0]
1081
1082
1083def get_test_description(descriptions, test):
1084 short_description = test.shortDescription()
1085 if descriptions and short_description:
1086 return short_description
1087 else:
1088 return str(test)
1089
1090
Dave Wallace8800f732023-08-31 00:47:44 -04001091def get_failed_testcase_linkname(failed_dir, testcase_dirname):
1092 return os.path.join(failed_dir, f"{testcase_dirname}-FAILED")
1093
1094
1095def get_testcase_dirname(testcase_class_name):
1096 return f"vpp-unittest-{testcase_class_name}"
1097
1098
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001099class TestCaseInfo(object):
1100 def __init__(self, logger, tempdir, vpp_pid, vpp_bin_path):
1101 self.logger = logger
1102 self.tempdir = tempdir
1103 self.vpp_pid = vpp_pid
1104 self.vpp_bin_path = vpp_bin_path
1105 self.core_crash_test = None
1106
1107
1108class VppTestResult(unittest.TestResult):
1109 """
1110 @property result_string
1111 String variable to store the test case result string.
1112 @property errors
1113 List variable containing 2-tuples of TestCase instances and strings
1114 holding formatted tracebacks. Each tuple represents a test which
1115 raised an unexpected exception.
1116 @property failures
1117 List variable containing 2-tuples of TestCase instances and strings
1118 holding formatted tracebacks. Each tuple represents a test where
1119 a failure was explicitly signalled using the TestCase.assert*()
1120 methods.
1121 """
1122
1123 failed_test_cases_info = set()
1124 core_crash_test_cases_info = set()
1125 current_test_case_info = None
1126
1127 def __init__(self, stream=None, descriptions=None, verbosity=None, runner=None):
1128 """
1129 :param stream File descriptor to store where to report test results.
1130 Set to the standard error stream by default.
1131 :param descriptions Boolean variable to store information if to use
1132 test case descriptions.
1133 :param verbosity Integer variable to store required verbosity level.
1134 """
1135 super(VppTestResult, self).__init__(stream, descriptions, verbosity)
1136 self.stream = stream
1137 self.descriptions = descriptions
1138 self.verbosity = verbosity
Klement Sekera47f35272023-03-29 16:04:58 +02001139 self.result_code = TestResultCode.TEST_RUN
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001140 self.result_string = None
1141 self.runner = runner
1142 self.printed = []
1143
Dave Wallace8800f732023-08-31 00:47:44 -04001144 def decodePcapFiles(self, test, when_configured=False):
1145 if when_configured == False or config.decode_pcaps == True:
1146 if hasattr(test, "pg_interfaces") and len(test.pg_interfaces) > 0:
1147 testcase_dir = os.path.dirname(test.pg_interfaces[0].out_path)
1148 test.pg_interfaces[0].decode_pcap_files(
1149 testcase_dir, f"suite{test.__class__.__name__}"
1150 )
1151 test.pg_interfaces[0].decode_pcap_files(
1152 testcase_dir, test._testMethodName
1153 )
1154
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001155 def addSuccess(self, test):
1156 """
1157 Record a test succeeded result
1158
1159 :param test:
1160
1161 """
Klement Sekera47f35272023-03-29 16:04:58 +02001162 self.log_result("addSuccess", test)
Dave Wallace8800f732023-08-31 00:47:44 -04001163 self.decodePcapFiles(test, when_configured=True)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001164 unittest.TestResult.addSuccess(self, test)
1165 self.result_string = colorize("OK", GREEN)
Klement Sekera47f35272023-03-29 16:04:58 +02001166 self.result_code = TestResultCode.PASS
1167 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001168
Klement Sekera47f35272023-03-29 16:04:58 +02001169 def addExpectedFailure(self, test, err):
1170 self.log_result("addExpectedFailure", test, err)
Dave Wallace8800f732023-08-31 00:47:44 -04001171 self.decodePcapFiles(test)
Klement Sekera47f35272023-03-29 16:04:58 +02001172 super().addExpectedFailure(test, err)
1173 self.result_string = colorize("FAIL", GREEN)
1174 self.result_code = TestResultCode.EXPECTED_FAIL
1175 self.send_result_through_pipe(test, self.result_code)
1176
1177 def addUnexpectedSuccess(self, test):
1178 self.log_result("addUnexpectedSuccess", test)
Dave Wallace8800f732023-08-31 00:47:44 -04001179 self.decodePcapFiles(test, when_configured=True)
Klement Sekera47f35272023-03-29 16:04:58 +02001180 super().addUnexpectedSuccess(test)
1181 self.result_string = colorize("OK", RED)
1182 self.result_code = TestResultCode.UNEXPECTED_PASS
1183 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001184
1185 def addSkip(self, test, reason):
1186 """
1187 Record a test skipped.
1188
1189 :param test:
1190 :param reason:
1191
1192 """
Klement Sekera47f35272023-03-29 16:04:58 +02001193 self.log_result("addSkip", test, reason=reason)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001194 unittest.TestResult.addSkip(self, test, reason)
1195 self.result_string = colorize("SKIP", YELLOW)
1196
1197 if reason == "not enough cpus":
Klement Sekera47f35272023-03-29 16:04:58 +02001198 self.result_code = TestResultCode.SKIP_CPU_SHORTAGE
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001199 else:
Klement Sekera47f35272023-03-29 16:04:58 +02001200 self.result_code = TestResultCode.SKIP
1201 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001202
1203 def symlink_failed(self):
1204 if self.current_test_case_info:
1205 try:
1206 failed_dir = config.failed_dir
Dave Wallace8800f732023-08-31 00:47:44 -04001207 link_path = get_failed_testcase_linkname(
1208 failed_dir, os.path.basename(self.current_test_case_info.tempdir)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001209 )
1210
1211 self.current_test_case_info.logger.debug(
1212 "creating a link to the failed test"
1213 )
1214 self.current_test_case_info.logger.debug(
1215 "os.symlink(%s, %s)"
1216 % (self.current_test_case_info.tempdir, link_path)
1217 )
1218 if os.path.exists(link_path):
1219 self.current_test_case_info.logger.debug("symlink already exists")
1220 else:
1221 os.symlink(self.current_test_case_info.tempdir, link_path)
1222
1223 except Exception as e:
1224 self.current_test_case_info.logger.error(e)
1225
1226 def send_result_through_pipe(self, test, result):
1227 if hasattr(self, "test_framework_result_pipe"):
1228 pipe = self.test_framework_result_pipe
1229 if pipe:
1230 pipe.send((test.id(), result))
1231
Klement Sekera47f35272023-03-29 16:04:58 +02001232 def log_result(self, fn, test, err=None, reason=None):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001233 if self.current_test_case_info:
1234 if isinstance(test, unittest.suite._ErrorHolder):
1235 test_name = test.description
1236 else:
1237 test_name = "%s.%s(%s)" % (
1238 test.__class__.__name__,
1239 test._testMethodName,
1240 test._testMethodDoc,
1241 )
Klement Sekera47f35272023-03-29 16:04:58 +02001242 extra_msg = ""
1243 if err:
1244 extra_msg += f", error is {err}"
1245 if reason:
1246 extra_msg += f", reason is {reason}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001247 self.current_test_case_info.logger.debug(
Klement Sekera47f35272023-03-29 16:04:58 +02001248 f"--- {fn}() {test_name} called{extra_msg}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001249 )
Klement Sekera47f35272023-03-29 16:04:58 +02001250 if err:
1251 self.current_test_case_info.logger.debug(
1252 "formatted exception is:\n%s" % "".join(format_exception(*err))
1253 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001254
Klement Sekera47f35272023-03-29 16:04:58 +02001255 def add_error(self, test, err, unittest_fn, result_code):
1256 self.result_code = result_code
1257 if result_code == TestResultCode.FAIL:
1258 self.log_result("addFailure", test, err=err)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001259 error_type_str = colorize("FAIL", RED)
Klement Sekera47f35272023-03-29 16:04:58 +02001260 elif result_code == TestResultCode.ERROR:
1261 self.log_result("addError", test, err=err)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001262 error_type_str = colorize("ERROR", RED)
1263 else:
Klement Sekera47f35272023-03-29 16:04:58 +02001264 raise Exception(f"Unexpected result code {result_code}")
Dave Wallace8800f732023-08-31 00:47:44 -04001265 self.decodePcapFiles(test)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001266
1267 unittest_fn(self, test, err)
1268 if self.current_test_case_info:
1269 self.result_string = "%s [ temp dir used by test case: %s ]" % (
1270 error_type_str,
1271 self.current_test_case_info.tempdir,
1272 )
1273 self.symlink_failed()
1274 self.failed_test_cases_info.add(self.current_test_case_info)
1275 if is_core_present(self.current_test_case_info.tempdir):
1276 if not self.current_test_case_info.core_crash_test:
1277 if isinstance(test, unittest.suite._ErrorHolder):
1278 test_name = str(test)
1279 else:
1280 test_name = "'{!s}' ({!s})".format(
1281 get_testcase_doc_name(test), test.id()
1282 )
1283 self.current_test_case_info.core_crash_test = test_name
1284 self.core_crash_test_cases_info.add(self.current_test_case_info)
1285 else:
1286 self.result_string = "%s [no temp dir]" % error_type_str
1287
Klement Sekera47f35272023-03-29 16:04:58 +02001288 self.send_result_through_pipe(test, result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001289
1290 def addFailure(self, test, err):
1291 """
1292 Record a test failed result
1293
1294 :param test:
1295 :param err: error message
1296
1297 """
Klement Sekera47f35272023-03-29 16:04:58 +02001298 self.add_error(test, err, unittest.TestResult.addFailure, TestResultCode.FAIL)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001299
1300 def addError(self, test, err):
1301 """
1302 Record a test error result
1303
1304 :param test:
1305 :param err: error message
1306
1307 """
Klement Sekera47f35272023-03-29 16:04:58 +02001308 self.add_error(test, err, unittest.TestResult.addError, TestResultCode.ERROR)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001309
1310 def getDescription(self, test):
1311 """
1312 Get test description
1313
1314 :param test:
1315 :returns: test description
1316
1317 """
1318 return get_test_description(self.descriptions, test)
1319
1320 def startTest(self, test):
1321 """
1322 Start a test
1323
1324 :param test:
1325
1326 """
1327
1328 def print_header(test):
1329 if test.__class__ in self.printed:
1330 return
1331
1332 test_doc = getdoc(test)
1333 if not test_doc:
1334 raise Exception("No doc string for test '%s'" % test.id())
1335
1336 test_title = test_doc.splitlines()[0].rstrip()
1337 test_title = colorize(test_title, GREEN)
1338 if test.is_tagged_run_solo():
1339 test_title = colorize(f"SOLO RUN: {test_title}", YELLOW)
1340
1341 # This block may overwrite the colorized title above,
1342 # but we want this to stand out and be fixed
1343 if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
1344 test_title = colorize(f"FIXME with VPP workers: {test_title}", RED)
1345
1346 if test.has_tag(TestCaseTag.FIXME_ASAN):
1347 test_title = colorize(f"FIXME with ASAN: {test_title}", RED)
1348 test.skip_fixme_asan()
1349
1350 if hasattr(test, "vpp_worker_count"):
1351 if test.vpp_worker_count == 0:
1352 test_title += " [main thread only]"
1353 elif test.vpp_worker_count == 1:
1354 test_title += " [1 worker thread]"
1355 else:
1356 test_title += f" [{test.vpp_worker_count} worker threads]"
1357
1358 if test.__class__.skipped_due_to_cpu_lack:
1359 test_title = colorize(
1360 f"{test_title} [skipped - not enough cpus, "
1361 f"required={test.__class__.get_cpus_required()}, "
1362 f"available={max_vpp_cpus}]",
1363 YELLOW,
1364 )
1365
1366 print(double_line_delim)
1367 print(test_title)
1368 print(double_line_delim)
1369 self.printed.append(test.__class__)
1370
1371 print_header(test)
1372 self.start_test = time.time()
1373 unittest.TestResult.startTest(self, test)
1374 if self.verbosity > 0:
1375 self.stream.writeln("Starting " + self.getDescription(test) + " ...")
1376 self.stream.writeln(single_line_delim)
1377
1378 def stopTest(self, test):
1379 """
1380 Called when the given test has been run
1381
1382 :param test:
1383
1384 """
1385 unittest.TestResult.stopTest(self, test)
1386
Klement Sekera47f35272023-03-29 16:04:58 +02001387 result_code_to_suffix = {
1388 TestResultCode.PASS: "",
1389 TestResultCode.FAIL: "",
1390 TestResultCode.ERROR: "",
1391 TestResultCode.SKIP: "",
1392 TestResultCode.TEST_RUN: "",
1393 TestResultCode.SKIP_CPU_SHORTAGE: "",
1394 TestResultCode.EXPECTED_FAIL: " [EXPECTED FAIL]",
1395 TestResultCode.UNEXPECTED_PASS: " [UNEXPECTED PASS]",
1396 }
1397
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001398 if self.verbosity > 0:
1399 self.stream.writeln(single_line_delim)
1400 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001401 "%-72s%s%s"
1402 % (
1403 self.getDescription(test),
1404 self.result_string,
1405 result_code_to_suffix[self.result_code],
1406 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001407 )
1408 self.stream.writeln(single_line_delim)
1409 else:
1410 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001411 "%-67s %4.2f %s%s"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001412 % (
1413 self.getDescription(test),
1414 time.time() - self.start_test,
1415 self.result_string,
Klement Sekera47f35272023-03-29 16:04:58 +02001416 result_code_to_suffix[self.result_code],
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001417 )
1418 )
1419
Klement Sekera47f35272023-03-29 16:04:58 +02001420 self.send_result_through_pipe(test, TestResultCode.TEST_RUN)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001421
1422 def printErrors(self):
1423 """
1424 Print errors from running the test case
1425 """
1426 if len(self.errors) > 0 or len(self.failures) > 0:
1427 self.stream.writeln()
1428 self.printErrorList("ERROR", self.errors)
1429 self.printErrorList("FAIL", self.failures)
1430
1431 # ^^ that is the last output from unittest before summary
1432 if not self.runner.print_summary:
1433 devnull = unittest.runner._WritelnDecorator(open(os.devnull, "w"))
1434 self.stream = devnull
1435 self.runner.stream = devnull
1436
1437 def printErrorList(self, flavour, errors):
1438 """
1439 Print error list to the output stream together with error type
1440 and test case description.
1441
1442 :param flavour: error type
1443 :param errors: iterable errors
1444
1445 """
1446 for test, err in errors:
1447 self.stream.writeln(double_line_delim)
1448 self.stream.writeln("%s: %s" % (flavour, self.getDescription(test)))
1449 self.stream.writeln(single_line_delim)
1450 self.stream.writeln("%s" % err)
1451
1452
1453class VppTestRunner(unittest.TextTestRunner):
1454 """
1455 A basic test runner implementation which prints results to standard error.
1456 """
1457
1458 @property
1459 def resultclass(self):
1460 """Class maintaining the results of the tests"""
1461 return VppTestResult
1462
1463 def __init__(
1464 self,
1465 keep_alive_pipe=None,
1466 descriptions=True,
1467 verbosity=1,
1468 result_pipe=None,
1469 failfast=False,
1470 buffer=False,
1471 resultclass=None,
1472 print_summary=True,
1473 **kwargs,
1474 ):
1475 # ignore stream setting here, use hard-coded stdout to be in sync
Dave Wallace8800f732023-08-31 00:47:44 -04001476 # with prints from VppAsfTestCase methods ...
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001477 super(VppTestRunner, self).__init__(
1478 sys.stdout, descriptions, verbosity, failfast, buffer, resultclass, **kwargs
1479 )
1480 KeepAliveReporter.pipe = keep_alive_pipe
1481
1482 self.orig_stream = self.stream
1483 self.resultclass.test_framework_result_pipe = result_pipe
1484
1485 self.print_summary = print_summary
1486
1487 def _makeResult(self):
1488 return self.resultclass(self.stream, self.descriptions, self.verbosity, self)
1489
1490 def run(self, test):
1491 """
1492 Run the tests
1493
1494 :param test:
1495
1496 """
1497 faulthandler.enable() # emit stack trace to stderr if killed by signal
1498
1499 result = super(VppTestRunner, self).run(test)
1500 if not self.print_summary:
1501 self.stream = self.orig_stream
1502 result.stream = self.orig_stream
1503 return result
1504
1505
1506class Worker(Thread):
1507 def __init__(self, executable_args, logger, env=None, *args, **kwargs):
1508 super(Worker, self).__init__(*args, **kwargs)
1509 self.logger = logger
1510 self.args = executable_args
1511 if hasattr(self, "testcase") and self.testcase.debug_all:
1512 if self.testcase.debug_gdbserver:
1513 self.args = [
1514 "/usr/bin/gdbserver",
1515 "localhost:{port}".format(port=self.testcase.gdbserver_port),
1516 ] + args
1517 elif self.testcase.debug_gdb and hasattr(self, "wait_for_gdb"):
1518 self.args.append(self.wait_for_gdb)
1519 self.app_bin = executable_args[0]
1520 self.app_name = os.path.basename(self.app_bin)
1521 if hasattr(self, "role"):
1522 self.app_name += " {role}".format(role=self.role)
1523 self.process = None
1524 self.result = None
1525 env = {} if env is None else env
1526 self.env = copy.deepcopy(env)
1527
1528 def wait_for_enter(self):
1529 if not hasattr(self, "testcase"):
1530 return
1531 if self.testcase.debug_all and self.testcase.debug_gdbserver:
1532 print()
1533 print(double_line_delim)
1534 print(
1535 "Spawned GDB Server for '{app}' with PID: {pid}".format(
1536 app=self.app_name, pid=self.process.pid
1537 )
1538 )
1539 elif self.testcase.debug_all and self.testcase.debug_gdb:
1540 print()
1541 print(double_line_delim)
1542 print(
1543 "Spawned '{app}' with PID: {pid}".format(
1544 app=self.app_name, pid=self.process.pid
1545 )
1546 )
1547 else:
1548 return
1549 print(single_line_delim)
1550 print("You can debug '{app}' using:".format(app=self.app_name))
1551 if self.testcase.debug_gdbserver:
1552 print(
1553 "sudo gdb "
1554 + self.app_bin
1555 + " -ex 'target remote localhost:{port}'".format(
1556 port=self.testcase.gdbserver_port
1557 )
1558 )
1559 print(
1560 "Now is the time to attach gdb by running the above "
1561 "command, set up breakpoints etc., then resume from "
1562 "within gdb by issuing the 'continue' command"
1563 )
1564 self.testcase.gdbserver_port += 1
1565 elif self.testcase.debug_gdb:
1566 print(
1567 "sudo gdb "
1568 + self.app_bin
1569 + " -ex 'attach {pid}'".format(pid=self.process.pid)
1570 )
1571 print(
1572 "Now is the time to attach gdb by running the above "
1573 "command and set up breakpoints etc., then resume from"
1574 " within gdb by issuing the 'continue' command"
1575 )
1576 print(single_line_delim)
1577 input("Press ENTER to continue running the testcase...")
1578
1579 def run(self):
1580 executable = self.args[0]
1581 if not os.path.exists(executable) or not os.access(
1582 executable, os.F_OK | os.X_OK
1583 ):
1584 # Exit code that means some system file did not exist,
1585 # could not be opened, or had some other kind of error.
1586 self.result = os.EX_OSFILE
1587 raise EnvironmentError(
1588 "executable '%s' is not found or executable." % executable
1589 )
1590 self.logger.debug(
1591 "Running executable '{app}': '{cmd}'".format(
1592 app=self.app_name, cmd=" ".join(self.args)
1593 )
1594 )
1595 env = os.environ.copy()
1596 env.update(self.env)
1597 env["CK_LOG_FILE_NAME"] = "-"
1598 self.process = subprocess.Popen(
1599 ["stdbuf", "-o0", "-e0"] + self.args,
1600 shell=False,
1601 env=env,
1602 preexec_fn=os.setpgrp,
1603 stdout=subprocess.PIPE,
1604 stderr=subprocess.PIPE,
1605 )
1606 self.wait_for_enter()
1607 out, err = self.process.communicate()
1608 self.logger.debug("Finished running `{app}'".format(app=self.app_name))
1609 self.logger.info("Return code is `%s'" % self.process.returncode)
1610 self.logger.info(single_line_delim)
1611 self.logger.info(
1612 "Executable `{app}' wrote to stdout:".format(app=self.app_name)
1613 )
1614 self.logger.info(single_line_delim)
1615 self.logger.info(out.decode("utf-8"))
1616 self.logger.info(single_line_delim)
1617 self.logger.info(
1618 "Executable `{app}' wrote to stderr:".format(app=self.app_name)
1619 )
1620 self.logger.info(single_line_delim)
1621 self.logger.info(err.decode("utf-8"))
1622 self.logger.info(single_line_delim)
1623 self.result = self.process.returncode
1624
1625
1626if __name__ == "__main__":
1627 pass