blob: bd1b45c6476f7c8620849e4c21cf8c02ca3277db [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
Dave Wallace85ce9312024-08-19 18:47:55 -0400169def _is_distro_ubuntu2404():
170 with open("/etc/os-release") as f:
171 for line in f.readlines():
172 if "noble" in line:
173 return True
174 return False
175
176
177is_distro_ubuntu2404 = _is_distro_ubuntu2404()
178
179
Dave Wallace8800f732023-08-31 00:47:44 -0400180def _is_distro_debian11():
181 with open("/etc/os-release") as f:
182 for line in f.readlines():
183 if "bullseye" in line:
184 return True
185 return False
186
187
188is_distro_debian11 = _is_distro_debian11()
189
190
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400191class KeepAliveReporter(object):
192 """
193 Singleton object which reports test start to parent process
194 """
195
196 _shared_state = {}
197
198 def __init__(self):
199 self.__dict__ = self._shared_state
200 self._pipe = None
201
202 @property
203 def pipe(self):
204 return self._pipe
205
206 @pipe.setter
207 def pipe(self, pipe):
208 if self._pipe is not None:
209 raise Exception("Internal error - pipe should only be set once.")
210 self._pipe = pipe
211
212 def send_keep_alive(self, test, desc=None):
213 """
214 Write current test tmpdir & desc to keep-alive pipe to signal liveness
215 """
216 if self.pipe is None:
217 # if not running forked..
218 return
219
220 if isclass(test):
221 desc = "%s (%s)" % (desc, unittest.util.strclass(test))
222 else:
223 desc = test.id()
224
225 self.pipe.send((desc, config.vpp, test.tempdir, test.vpp.pid))
226
227
228class TestCaseTag(Enum):
229 # marks the suites that must run at the end
230 # using only a single test runner
231 RUN_SOLO = 1
232 # marks the suites broken on VPP multi-worker
233 FIXME_VPP_WORKERS = 2
234 # marks the suites broken when ASan is enabled
235 FIXME_ASAN = 3
Dave Wallace8800f732023-08-31 00:47:44 -0400236 # marks suites broken on Ubuntu-22.04
237 FIXME_UBUNTU2204 = 4
238 # marks suites broken on Debian-11
239 FIXME_DEBIAN11 = 5
240 # marks suites broken on debug vpp image
241 FIXME_VPP_DEBUG = 6
Dave Wallace85ce9312024-08-19 18:47:55 -0400242 # marks suites broken on Ubuntu-24.04
243 FIXME_UBUNTU2404 = 7
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400244
245
246def create_tag_decorator(e):
247 def decorator(cls):
248 try:
249 cls.test_tags.append(e)
250 except AttributeError:
251 cls.test_tags = [e]
252 return cls
253
254 return decorator
255
256
257tag_run_solo = create_tag_decorator(TestCaseTag.RUN_SOLO)
258tag_fixme_vpp_workers = create_tag_decorator(TestCaseTag.FIXME_VPP_WORKERS)
259tag_fixme_asan = create_tag_decorator(TestCaseTag.FIXME_ASAN)
Dave Wallace8800f732023-08-31 00:47:44 -0400260tag_fixme_ubuntu2204 = create_tag_decorator(TestCaseTag.FIXME_UBUNTU2204)
261tag_fixme_debian11 = create_tag_decorator(TestCaseTag.FIXME_DEBIAN11)
262tag_fixme_vpp_debug = create_tag_decorator(TestCaseTag.FIXME_VPP_DEBUG)
Dave Wallace85ce9312024-08-19 18:47:55 -0400263tag_fixme_ubuntu2404 = create_tag_decorator(TestCaseTag.FIXME_UBUNTU2404)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400264
265
266class DummyVpp:
267 returncode = None
268 pid = 0xCAFEBAFE
269
270 def poll(self):
271 pass
272
273 def terminate(self):
274 pass
275
276
277class CPUInterface(ABC):
278 cpus = []
279 skipped_due_to_cpu_lack = False
280
281 @classmethod
282 @abstractmethod
283 def get_cpus_required(cls):
284 pass
285
286 @classmethod
287 def assign_cpus(cls, cpus):
288 cls.cpus = cpus
289
290
Dave Wallace8800f732023-08-31 00:47:44 -0400291class VppAsfTestCase(CPUInterface, unittest.TestCase):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400292 """This subclass is a base class for VPP test cases that are implemented as
293 classes. It provides methods to create and run test case.
294 """
295
296 extra_vpp_statseg_config = ""
Klement Sekerad3e0d102023-01-26 12:35:35 +0100297 extra_vpp_config = []
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400298 extra_vpp_plugin_config = []
299 logger = null_logger
300 vapi_response_timeout = 5
301 remove_configured_vpp_objects_on_tear_down = True
302
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400303 @classmethod
304 def has_tag(cls, tag):
305 """if the test case has a given tag - return true"""
306 try:
307 return tag in cls.test_tags
308 except AttributeError:
309 pass
310 return False
311
312 @classmethod
313 def is_tagged_run_solo(cls):
314 """if the test case class is timing-sensitive - return true"""
315 return cls.has_tag(TestCaseTag.RUN_SOLO)
316
317 @classmethod
318 def skip_fixme_asan(cls):
319 """if @tag_fixme_asan & ASan is enabled - mark for skip"""
320 if cls.has_tag(TestCaseTag.FIXME_ASAN):
321 vpp_extra_cmake_args = os.environ.get("VPP_EXTRA_CMAKE_ARGS", "")
322 if "DVPP_ENABLE_SANITIZE_ADDR=ON" in vpp_extra_cmake_args:
323 cls = unittest.skip("Skipping @tag_fixme_asan tests")(cls)
324
325 @classmethod
Andrew Yourtchenko9987d472024-07-02 18:02:09 +0200326 def skip_fixme_ubuntu2204(cls):
327 """if @tag_fixme_ubuntu2204 & is Ubuntu22.04 - mark for skip"""
328 if cls.has_tag(TestCaseTag.FIXME_UBUNTU2204) and is_distro_ubuntu2204 == True:
329 cls = unittest.skip("Skipping @tag_fixme_ubuntu2204 tests")(cls)
330
331 @classmethod
Dave Wallace85ce9312024-08-19 18:47:55 -0400332 def skip_fixme_ubuntu2404(cls):
333 """if @tag_fixme_ubuntu2404 & is Ubuntu24.04 - mark for skip"""
334 if cls.has_tag(TestCaseTag.FIXME_UBUNTU2404) and is_distro_ubuntu2404 == True:
335 cls = unittest.skip("Skipping @tag_fixme_ubuntu2404 tests")(cls)
336
337 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400338 def instance(cls):
339 """Return the instance of this testcase"""
340 return cls.test_instance
341
342 @classmethod
343 def set_debug_flags(cls, d):
344 cls.gdbserver_port = 7777
345 cls.debug_core = False
346 cls.debug_gdb = False
347 cls.debug_gdbserver = False
348 cls.debug_all = False
349 cls.debug_attach = False
350 if d is None:
351 return
352 dl = d.lower()
353 if dl == "core":
354 cls.debug_core = True
355 elif dl == "gdb" or dl == "gdb-all":
356 cls.debug_gdb = True
357 elif dl == "gdbserver" or dl == "gdbserver-all":
358 cls.debug_gdbserver = True
359 elif dl == "attach":
360 cls.debug_attach = True
361 else:
362 raise Exception("Unrecognized DEBUG option: '%s'" % d)
363 if dl == "gdb-all" or dl == "gdbserver-all":
364 cls.debug_all = True
365
366 @classmethod
367 def get_vpp_worker_count(cls):
368 if not hasattr(cls, "vpp_worker_count"):
369 if cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
370 cls.vpp_worker_count = 0
371 else:
372 cls.vpp_worker_count = config.vpp_worker_count
373 return cls.vpp_worker_count
374
375 @classmethod
376 def get_cpus_required(cls):
377 return 1 + cls.get_vpp_worker_count()
378
379 @classmethod
380 def setUpConstants(cls):
381 """Set-up the test case class based on environment variables"""
382 cls.step = config.step
383 cls.plugin_path = ":".join(config.vpp_plugin_dir)
384 cls.test_plugin_path = ":".join(config.vpp_test_plugin_dir)
385 cls.extern_plugin_path = ":".join(config.extern_plugin_dir)
386 debug_cli = ""
387 if cls.step or cls.debug_gdb or cls.debug_gdbserver:
388 debug_cli = "cli-listen localhost:5002"
389 size = re.search(r"\d+[gG]", config.coredump_size)
390 if size:
391 coredump_size = f"coredump-size {config.coredump_size}".lower()
392 else:
393 coredump_size = "coredump-size unlimited"
394 default_variant = config.variant
395 if default_variant is not None:
Dmitry Valter953fbd22021-09-19 23:50:57 +0300396 default_variant = "default { variant %s 100 }" % default_variant
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400397 else:
398 default_variant = ""
399
400 api_fuzzing = config.api_fuzz
401 if api_fuzzing is None:
402 api_fuzzing = "off"
403
404 cls.vpp_cmdline = [
405 config.vpp,
406 "unix",
407 "{",
408 "nodaemon",
409 debug_cli,
410 "full-coredump",
411 coredump_size,
412 "runtime-dir",
413 cls.tempdir,
414 "}",
415 "api-trace",
416 "{",
417 "on",
418 "}",
419 "api-segment",
420 "{",
421 "prefix",
422 cls.get_api_segment_prefix(),
423 "}",
424 "cpu",
425 "{",
426 "main-core",
427 str(cls.cpus[0]),
428 ]
429 if cls.extern_plugin_path not in (None, ""):
430 cls.extra_vpp_plugin_config.append("add-path %s" % cls.extern_plugin_path)
431 if cls.get_vpp_worker_count():
432 cls.vpp_cmdline.extend(
433 ["corelist-workers", ",".join([str(x) for x in cls.cpus[1:]])]
434 )
435 cls.vpp_cmdline.extend(
436 [
437 "}",
438 "physmem",
439 "{",
440 "max-size",
441 "32m",
442 "}",
443 "statseg",
444 "{",
445 "socket-name",
446 cls.get_stats_sock_path(),
447 cls.extra_vpp_statseg_config,
448 "}",
449 "socksvr",
450 "{",
451 "socket-name",
452 cls.get_api_sock_path(),
453 "}",
454 "node { ",
455 default_variant,
456 "}",
457 "api-fuzz {",
458 api_fuzzing,
459 "}",
460 "plugins",
461 "{",
462 "plugin",
463 "dpdk_plugin.so",
464 "{",
465 "disable",
466 "}",
467 "plugin",
468 "rdma_plugin.so",
469 "{",
470 "disable",
471 "}",
472 "plugin",
473 "lisp_unittest_plugin.so",
474 "{",
475 "enable",
476 "}",
477 "plugin",
478 "unittest_plugin.so",
479 "{",
480 "enable",
481 "}",
482 ]
483 + cls.extra_vpp_plugin_config
484 + [
485 "}",
486 ]
487 )
488
Klement Sekerad3e0d102023-01-26 12:35:35 +0100489 if cls.extra_vpp_config is not None:
490 cls.vpp_cmdline.extend(cls.extra_vpp_config)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400491
492 if not cls.debug_attach:
493 cls.logger.info("vpp_cmdline args: %s" % cls.vpp_cmdline)
494 cls.logger.info("vpp_cmdline: %s" % " ".join(cls.vpp_cmdline))
495
496 @classmethod
497 def wait_for_enter(cls):
498 if cls.debug_gdbserver:
499 print(double_line_delim)
500 print("Spawned GDB server with PID: %d" % cls.vpp.pid)
501 elif cls.debug_gdb:
502 print(double_line_delim)
503 print("Spawned VPP with PID: %d" % cls.vpp.pid)
504 else:
505 cls.logger.debug("Spawned VPP with PID: %d" % cls.vpp.pid)
506 return
507 print(single_line_delim)
508 print("You can debug VPP using:")
509 if cls.debug_gdbserver:
510 print(
511 f"sudo gdb {config.vpp} "
512 f"-ex 'target remote localhost:{cls.gdbserver_port}'"
513 )
514 print(
515 "Now is the time to attach gdb by running the above "
516 "command, set up breakpoints etc., then resume VPP from "
517 "within gdb by issuing the 'continue' command"
518 )
519 cls.gdbserver_port += 1
520 elif cls.debug_gdb:
521 print(f"sudo gdb {config.vpp} -ex 'attach {cls.vpp.pid}'")
522 print(
523 "Now is the time to attach gdb by running the above "
524 "command and set up breakpoints etc., then resume VPP from"
525 " within gdb by issuing the 'continue' command"
526 )
527 print(single_line_delim)
528 input("Press ENTER to continue running the testcase...")
529
530 @classmethod
531 def attach_vpp(cls):
532 cls.vpp = DummyVpp()
533
534 @classmethod
535 def run_vpp(cls):
536 cls.logger.debug(f"Assigned cpus: {cls.cpus}")
537 cmdline = cls.vpp_cmdline
538
539 if cls.debug_gdbserver:
540 gdbserver = "/usr/bin/gdbserver"
541 if not os.path.isfile(gdbserver) or not os.access(gdbserver, os.X_OK):
542 raise Exception(
543 "gdbserver binary '%s' does not exist or is "
544 "not executable" % gdbserver
545 )
546
547 cmdline = [
548 gdbserver,
549 "localhost:{port}".format(port=cls.gdbserver_port),
550 ] + cls.vpp_cmdline
551 cls.logger.info("Gdbserver cmdline is %s", " ".join(cmdline))
552
553 try:
554 cls.vpp = subprocess.Popen(
555 cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE
556 )
557 except subprocess.CalledProcessError as e:
558 cls.logger.critical(
559 "Subprocess returned with non-0 return code: (%s)", e.returncode
560 )
561 raise
562 except OSError as e:
563 cls.logger.critical(
564 "Subprocess returned with OS error: (%s) %s", e.errno, e.strerror
565 )
566 raise
567 except Exception as e:
568 cls.logger.exception("Subprocess returned unexpected from %s:", cmdline)
569 raise
570
571 cls.wait_for_enter()
572
573 @classmethod
574 def wait_for_coredump(cls):
575 corefile = cls.tempdir + "/core"
576 if os.path.isfile(corefile):
577 cls.logger.error("Waiting for coredump to complete: %s", corefile)
578 curr_size = os.path.getsize(corefile)
579 deadline = time.time() + 60
580 ok = False
581 while time.time() < deadline:
582 cls.sleep(1)
583 size = curr_size
584 curr_size = os.path.getsize(corefile)
585 if size == curr_size:
586 ok = True
587 break
588 if not ok:
589 cls.logger.error(
590 "Timed out waiting for coredump to complete: %s", corefile
591 )
592 else:
593 cls.logger.error("Coredump complete: %s, size %d", corefile, curr_size)
594
595 @classmethod
596 def get_stats_sock_path(cls):
597 return "%s/stats.sock" % cls.tempdir
598
599 @classmethod
600 def get_api_sock_path(cls):
601 return "%s/api.sock" % cls.tempdir
602
603 @classmethod
Naveen Joy70335e82023-07-28 16:33:30 -0700604 def get_memif_sock_path(cls):
605 return "%s/memif.sock" % cls.tempdir
606
607 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400608 def get_api_segment_prefix(cls):
609 return os.path.basename(cls.tempdir) # Only used for VAPI
610
611 @classmethod
612 def get_tempdir(cls):
613 if cls.debug_attach:
614 tmpdir = f"{config.tmp_dir}/unittest-attach-gdb"
615 else:
Dave Wallace8800f732023-08-31 00:47:44 -0400616 tmpdir = f"{config.tmp_dir}/{get_testcase_dirname(cls.__name__)}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400617 if config.wipe_tmp_dir:
618 shutil.rmtree(tmpdir, ignore_errors=True)
619 os.mkdir(tmpdir)
620 return tmpdir
621
622 @classmethod
623 def create_file_handler(cls):
624 if config.log_dir is None:
625 cls.file_handler = FileHandler(f"{cls.tempdir}/log.txt")
626 return
627
Dave Wallace8800f732023-08-31 00:47:44 -0400628 logdir = f"{config.log_dir}/{get_testcase_dirname(cls.__name__)}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400629 if config.wipe_tmp_dir:
630 shutil.rmtree(logdir, ignore_errors=True)
631 os.mkdir(logdir)
632 cls.file_handler = FileHandler(f"{logdir}/log.txt")
633
634 @classmethod
635 def setUpClass(cls):
636 """
637 Perform class setup before running the testcase
638 Remove shared memory files, start vpp and connect the vpp-api
639 """
Dave Wallace8800f732023-08-31 00:47:44 -0400640 super(VppAsfTestCase, cls).setUpClass()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400641 cls.logger = get_logger(cls.__name__)
Dave Wallace8800f732023-08-31 00:47:44 -0400642 cls.logger.debug(f"--- START setUpClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400643 random.seed(config.rnd_seed)
644 if hasattr(cls, "parallel_handler"):
645 cls.logger.addHandler(cls.parallel_handler)
646 cls.logger.propagate = False
647 cls.set_debug_flags(config.debug)
648 cls.tempdir = cls.get_tempdir()
649 cls.create_file_handler()
650 cls.file_handler.setFormatter(
651 Formatter(fmt="%(asctime)s,%(msecs)03d %(message)s", datefmt="%H:%M:%S")
652 )
653 cls.file_handler.setLevel(DEBUG)
654 cls.logger.addHandler(cls.file_handler)
655 cls.logger.debug("--- setUpClass() for %s called ---" % cls.__name__)
656 os.chdir(cls.tempdir)
657 cls.logger.info(
658 "Temporary dir is %s, api socket is %s",
659 cls.tempdir,
660 cls.get_api_sock_path(),
661 )
662 cls.logger.debug("Random seed is %s", config.rnd_seed)
663 cls.setUpConstants()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400664 cls.verbose = 0
665 cls.vpp_dead = False
666 cls.registry = VppObjectRegistry()
667 cls.vpp_startup_failed = False
668 cls.reporter = KeepAliveReporter()
669 # need to catch exceptions here because if we raise, then the cleanup
670 # doesn't get called and we might end with a zombie vpp
671 try:
672 if cls.debug_attach:
673 cls.attach_vpp()
674 else:
675 cls.run_vpp()
676 cls.reporter.send_keep_alive(cls, "setUpClass")
677 VppTestResult.current_test_case_info = TestCaseInfo(
678 cls.logger, cls.tempdir, cls.vpp.pid, config.vpp
679 )
680 cls.vpp_stdout_deque = deque()
681 cls.vpp_stderr_deque = deque()
682 if not cls.debug_attach:
683 cls.pump_thread_stop_flag = Event()
684 cls.pump_thread_wakeup_pipe = os.pipe()
685 cls.pump_thread = Thread(target=pump_output, args=(cls,))
686 cls.pump_thread.daemon = True
687 cls.pump_thread.start()
688 if cls.debug_gdb or cls.debug_gdbserver or cls.debug_attach:
689 cls.vapi_response_timeout = 0
690 cls.vapi = VppPapiProvider(cls.__name__, cls, cls.vapi_response_timeout)
691 if cls.step:
692 hook = hookmodule.StepHook(cls)
693 else:
694 hook = hookmodule.PollHook(cls)
695 cls.vapi.register_hook(hook)
696 cls.statistics = VPPStats(socketname=cls.get_stats_sock_path())
697 try:
698 hook.poll_vpp()
699 except VppDiedError:
Dave Wallace8800f732023-08-31 00:47:44 -0400700 cls.wait_for_coredump()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400701 cls.vpp_startup_failed = True
702 cls.logger.critical(
703 "VPP died shortly after startup, check the"
704 " output to standard error for possible cause"
705 )
706 raise
707 try:
708 cls.vapi.connect()
709 except (vpp_papi.VPPIOError, Exception) as e:
710 cls.logger.debug("Exception connecting to vapi: %s" % e)
711 cls.vapi.disconnect()
712
713 if cls.debug_gdbserver:
714 print(
715 colorize(
716 "You're running VPP inside gdbserver but "
717 "VPP-API connection failed, did you forget "
718 "to 'continue' VPP from within gdb?",
719 RED,
720 )
721 )
722 raise e
723 if cls.debug_attach:
724 last_line = cls.vapi.cli("show thread").split("\n")[-2]
725 cls.vpp_worker_count = int(last_line.split(" ")[0])
726 print("Detected VPP with %s workers." % cls.vpp_worker_count)
727 except vpp_papi.VPPRuntimeError as e:
728 cls.logger.debug("%s" % e)
729 cls.quit()
730 raise e
731 except Exception as e:
732 cls.logger.debug("Exception connecting to VPP: %s" % e)
733 cls.quit()
734 raise e
Dave Wallace8800f732023-08-31 00:47:44 -0400735 cls.logger.debug(f"--- END setUpClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400736
737 @classmethod
738 def _debug_quit(cls):
739 if cls.debug_gdbserver or cls.debug_gdb:
740 try:
741 cls.vpp.poll()
742
743 if cls.vpp.returncode is None:
744 print()
745 print(double_line_delim)
746 print("VPP or GDB server is still running")
747 print(single_line_delim)
748 input(
749 "When done debugging, press ENTER to kill the "
750 "process and finish running the testcase..."
751 )
752 except AttributeError:
753 pass
754
755 @classmethod
756 def quit(cls):
757 """
758 Disconnect vpp-api, kill vpp and cleanup shared memory files
759 """
760 cls._debug_quit()
761
762 # first signal that we want to stop the pump thread, then wake it up
763 if hasattr(cls, "pump_thread_stop_flag"):
764 cls.pump_thread_stop_flag.set()
765 if hasattr(cls, "pump_thread_wakeup_pipe"):
766 os.write(cls.pump_thread_wakeup_pipe[1], b"ding dong wake up")
767 if hasattr(cls, "pump_thread"):
768 cls.logger.debug("Waiting for pump thread to stop")
769 cls.pump_thread.join()
770 if hasattr(cls, "vpp_stderr_reader_thread"):
771 cls.logger.debug("Waiting for stderr pump to stop")
772 cls.vpp_stderr_reader_thread.join()
773
774 if hasattr(cls, "vpp"):
775 if hasattr(cls, "vapi"):
776 cls.logger.debug(cls.vapi.vpp.get_stats())
777 cls.logger.debug("Disconnecting class vapi client on %s", cls.__name__)
778 cls.vapi.disconnect()
779 cls.logger.debug("Deleting class vapi attribute on %s", cls.__name__)
780 del cls.vapi
781 cls.vpp.poll()
782 if not cls.debug_attach and cls.vpp.returncode is None:
783 cls.wait_for_coredump()
784 cls.logger.debug("Sending TERM to vpp")
785 cls.vpp.terminate()
786 cls.logger.debug("Waiting for vpp to die")
787 try:
788 outs, errs = cls.vpp.communicate(timeout=5)
789 except subprocess.TimeoutExpired:
790 cls.vpp.kill()
791 outs, errs = cls.vpp.communicate()
792 cls.logger.debug("Deleting class vpp attribute on %s", cls.__name__)
793 if not cls.debug_attach:
794 cls.vpp.stdout.close()
795 cls.vpp.stderr.close()
796 del cls.vpp
797
798 if cls.vpp_startup_failed:
799 stdout_log = cls.logger.info
800 stderr_log = cls.logger.critical
801 else:
802 stdout_log = cls.logger.info
803 stderr_log = cls.logger.info
804
805 if hasattr(cls, "vpp_stdout_deque"):
806 stdout_log(single_line_delim)
807 stdout_log("VPP output to stdout while running %s:", cls.__name__)
808 stdout_log(single_line_delim)
809 vpp_output = "".join(cls.vpp_stdout_deque)
810 with open(cls.tempdir + "/vpp_stdout.txt", "w") as f:
811 f.write(vpp_output)
812 stdout_log("\n%s", vpp_output)
813 stdout_log(single_line_delim)
814
815 if hasattr(cls, "vpp_stderr_deque"):
816 stderr_log(single_line_delim)
817 stderr_log("VPP output to stderr while running %s:", cls.__name__)
818 stderr_log(single_line_delim)
819 vpp_output = "".join(cls.vpp_stderr_deque)
820 with open(cls.tempdir + "/vpp_stderr.txt", "w") as f:
821 f.write(vpp_output)
822 stderr_log("\n%s", vpp_output)
823 stderr_log(single_line_delim)
824
825 @classmethod
826 def tearDownClass(cls):
827 """Perform final cleanup after running all tests in this test-case"""
Dave Wallace8800f732023-08-31 00:47:44 -0400828 cls.logger.debug(f"--- START tearDownClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400829 cls.reporter.send_keep_alive(cls, "tearDownClass")
830 cls.quit()
831 cls.file_handler.close()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400832 if config.debug_framework:
833 debug_internal.on_tear_down_class(cls)
Dave Wallace8800f732023-08-31 00:47:44 -0400834 cls.logger.debug(f"--- END tearDownClass() {cls.__name__} ---")
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400835
836 def show_commands_at_teardown(self):
837 """Allow subclass specific teardown logging additions."""
838 self.logger.info("--- No test specific show commands provided. ---")
839
Dave Wallace87a88262023-11-27 20:23:03 -0500840 def unlink_testcase_file(self, path):
841 MAX_ATTEMPTS = 9
842 retries = MAX_ATTEMPTS
843 while retries > 0:
844 retries = retries - 1
845 self.logger.debug(f"Unlinking {path}")
846 try:
847 path.unlink()
848 # Loop until unlink() fails with FileNotFoundError to ensure file is removed
849 except FileNotFoundError:
850 break
851 except OSError:
852 self.logger.debug(f"OSError: unlinking {path}")
853 self.sleep(0.25, f"{retries} retries left")
854 if retries == 0 and os.path.isfile(path):
855 self.logger.error(
856 f"Unable to delete testcase file in {MAX_ATTEMPTS} attempts: {path}"
857 )
858 raise OSError
859
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400860 def tearDown(self):
861 """Show various debug prints after each test"""
862 self.logger.debug(
Dave Wallace8800f732023-08-31 00:47:44 -0400863 f"--- START tearDown() {self.__class__.__name__}.{self._testMethodName}({self._testMethodDoc}) ---"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400864 )
865
866 try:
867 if not self.vpp_dead:
868 self.logger.debug(self.vapi.cli("show trace max 1000"))
869 self.logger.info(self.vapi.ppcli("show interface"))
870 self.logger.info(self.vapi.ppcli("show hardware"))
871 self.logger.info(self.statistics.set_errors_str())
872 self.logger.info(self.vapi.ppcli("show run"))
873 self.logger.info(self.vapi.ppcli("show log"))
874 self.logger.info(self.vapi.ppcli("show bihash"))
875 self.logger.info("Logging testcase specific show commands.")
876 self.show_commands_at_teardown()
877 if self.remove_configured_vpp_objects_on_tear_down:
878 self.registry.remove_vpp_config(self.logger)
879 # Save/Dump VPP api trace log
880 m = self._testMethodName
881 api_trace = "vpp_api_trace.%s.%d.log" % (m, self.vpp.pid)
882 tmp_api_trace = "/tmp/%s" % api_trace
883 vpp_api_trace_log = "%s/%s" % (self.tempdir, api_trace)
884 self.logger.info(self.vapi.ppcli("api trace save %s" % api_trace))
885 self.logger.info("Moving %s to %s\n" % (tmp_api_trace, vpp_api_trace_log))
Dmitry Valter71d02aa2023-01-27 12:49:55 +0000886 shutil.move(tmp_api_trace, vpp_api_trace_log)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400887 except VppTransportSocketIOError:
888 self.logger.debug(
889 "VppTransportSocketIOError: Vpp dead. Cannot log show commands."
890 )
891 self.vpp_dead = True
892 else:
893 self.registry.unregister_all(self.logger)
Dave Wallace8800f732023-08-31 00:47:44 -0400894 # Remove any leftover pcap files
895 if hasattr(self, "pg_interfaces") and len(self.pg_interfaces) > 0:
896 testcase_dir = os.path.dirname(self.pg_interfaces[0].out_path)
897 for p in Path(testcase_dir).glob("pg*.pcap"):
Dave Wallace87a88262023-11-27 20:23:03 -0500898 self.unlink_testcase_file(p)
Dave Wallace8800f732023-08-31 00:47:44 -0400899 self.logger.debug(
900 f"--- END tearDown() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
901 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400902
903 def setUp(self):
904 """Clear trace before running each test"""
Dave Wallace8800f732023-08-31 00:47:44 -0400905 super(VppAsfTestCase, self).setUp()
906 self.logger.debug(
907 f"--- START setUp() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
908 )
909 # Save testname include in pcap history filenames
910 if hasattr(self, "pg_interfaces"):
911 for i in self.pg_interfaces:
912 i.test_name = self._testMethodName
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400913 self.reporter.send_keep_alive(self)
914 if self.vpp_dead:
Dave Wallace8800f732023-08-31 00:47:44 -0400915 self.wait_for_coredump()
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400916 raise VppDiedError(
917 rv=None,
918 testcase=self.__class__.__name__,
919 method_name=self._testMethodName,
920 )
921 self.sleep(0.1, "during setUp")
922 self.vpp_stdout_deque.append(
923 "--- test setUp() for %s.%s(%s) starts here ---\n"
924 % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)
925 )
926 self.vpp_stderr_deque.append(
927 "--- test setUp() for %s.%s(%s) starts here ---\n"
928 % (self.__class__.__name__, self._testMethodName, self._testMethodDoc)
929 )
930 self.vapi.cli("clear trace")
931 # store the test instance inside the test class - so that objects
932 # holding the class can access instance methods (like assertEqual)
933 type(self).test_instance = self
Dave Wallace8800f732023-08-31 00:47:44 -0400934 self.logger.debug(
935 f"--- END setUp() {self.__class__.__name__}.{self._testMethodName}('{self._testMethodDoc}') ---"
936 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400937
938 @classmethod
939 def get_vpp_time(cls):
940 # processes e.g. "Time now 2.190522, Wed, 11 Mar 2020 17:29:54 GMT"
941 # returns float("2.190522")
942 timestr = cls.vapi.cli("show clock")
943 head, sep, tail = timestr.partition(",")
944 head, sep, tail = head.partition("Time now")
945 return float(tail)
946
947 @classmethod
948 def sleep_on_vpp_time(cls, sec):
949 """Sleep according to time in VPP world"""
950 # On a busy system with many processes
951 # we might end up with VPP time being slower than real world
952 # So take that into account when waiting for VPP to do something
953 start_time = cls.get_vpp_time()
954 while cls.get_vpp_time() - start_time < sec:
955 cls.sleep(0.1)
956
957 @classmethod
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400958 def create_loopback_interfaces(cls, count):
959 """
960 Create loopback interfaces.
961
962 :param count: number of interfaces created.
963 :returns: List of created interfaces.
964 """
965 result = [VppLoInterface(cls) for i in range(count)]
966 for intf in result:
967 setattr(cls, intf.name, intf)
968 cls.lo_interfaces = result
969 return result
970
Pratikshya Prasai657bdf72022-08-18 11:09:38 -0400971 def assert_equal(self, real_value, expected_value, name_or_class=None):
972 if name_or_class is None:
973 self.assertEqual(real_value, expected_value)
974 return
975 try:
976 msg = "Invalid %s: %d('%s') does not match expected value %d('%s')"
977 msg = msg % (
978 getdoc(name_or_class).strip(),
979 real_value,
980 str(name_or_class(real_value)),
981 expected_value,
982 str(name_or_class(expected_value)),
983 )
984 except Exception:
985 msg = "Invalid %s: %s does not match expected value %s" % (
986 name_or_class,
987 real_value,
988 expected_value,
989 )
990
991 self.assertEqual(real_value, expected_value, msg)
992
993 def assert_in_range(self, real_value, expected_min, expected_max, name=None):
994 if name is None:
995 msg = None
996 else:
997 msg = "Invalid %s: %s out of range <%s,%s>" % (
998 name,
999 real_value,
1000 expected_min,
1001 expected_max,
1002 )
1003 self.assertTrue(expected_min <= real_value <= expected_max, msg)
1004
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001005 def get_counter(self, counter):
1006 if counter.startswith("/"):
1007 counter_value = self.statistics.get_counter(counter)
1008 else:
1009 counters = self.vapi.cli("sh errors").split("\n")
1010 counter_value = 0
1011 for i in range(1, len(counters) - 1):
1012 results = counters[i].split()
1013 if results[1] == counter:
1014 counter_value = int(results[0])
1015 break
1016 return counter_value
1017
1018 def assert_counter_equal(self, counter, expected_value, thread=None, index=0):
1019 c = self.get_counter(counter)
1020 if thread is not None:
1021 c = c[thread][index]
1022 else:
1023 c = sum(x[index] for x in c)
Klement Sekerac5fa5392023-08-17 19:38:34 +02001024 self.logger.debug(
1025 "validate counter `%s[%s]', expected: %s, real value: %s"
1026 % (counter, index, expected_value, c)
1027 )
1028 self.assert_equal(c, expected_value, "counter `%s[%s]'" % (counter, index))
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001029
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001030 def assert_error_counter_equal(self, counter, expected_value):
1031 counter_value = self.statistics[counter].sum()
1032 self.assert_equal(counter_value, expected_value, "error counter `%s'" % counter)
1033
1034 @classmethod
1035 def sleep(cls, timeout, remark=None):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001036 # /* Allow sleep(0) to maintain win32 semantics, and as decreed
1037 # * by Guido, only the main thread can be interrupted.
1038 # */
1039 # https://github.com/python/cpython/blob/6673decfa0fb078f60587f5cb5e98460eea137c2/Modules/timemodule.c#L1892 # noqa
1040 if timeout == 0:
1041 # yield quantum
1042 if hasattr(os, "sched_yield"):
1043 os.sched_yield()
1044 else:
1045 time.sleep(0)
1046 return
1047
1048 cls.logger.debug("Starting sleep for %es (%s)", timeout, remark)
1049 before = time.time()
1050 time.sleep(timeout)
1051 after = time.time()
1052 if after - before > 2 * timeout:
1053 cls.logger.error(
1054 "unexpected self.sleep() result - slept for %es instead of ~%es!",
1055 after - before,
1056 timeout,
1057 )
1058
1059 cls.logger.debug(
1060 "Finished sleep (%s) - slept %es (wanted %es)",
1061 remark,
1062 after - before,
1063 timeout,
1064 )
1065
1066 def virtual_sleep(self, timeout, remark=None):
1067 self.logger.debug("Moving VPP time by %s (%s)", timeout, remark)
1068 self.vapi.cli("set clock adjust %s" % timeout)
1069
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001070 def snapshot_stats(self, stats_diff):
1071 """Return snapshot of interesting stats based on diff dictionary."""
1072 stats_snapshot = {}
1073 for sw_if_index in stats_diff:
1074 for counter in stats_diff[sw_if_index]:
1075 stats_snapshot[counter] = self.statistics[counter]
1076 self.logger.debug(f"Took statistics stats_snapshot: {stats_snapshot}")
1077 return stats_snapshot
1078
1079 def compare_stats_with_snapshot(self, stats_diff, stats_snapshot):
1080 """Assert appropriate difference between current stats and snapshot."""
1081 for sw_if_index in stats_diff:
1082 for cntr, diff in stats_diff[sw_if_index].items():
1083 if sw_if_index == "err":
1084 self.assert_equal(
1085 self.statistics[cntr].sum(),
1086 stats_snapshot[cntr].sum() + diff,
1087 f"'{cntr}' counter value (previous value: "
1088 f"{stats_snapshot[cntr].sum()}, "
1089 f"expected diff: {diff})",
1090 )
1091 else:
1092 try:
1093 self.assert_equal(
1094 self.statistics[cntr][:, sw_if_index].sum(),
1095 stats_snapshot[cntr][:, sw_if_index].sum() + diff,
1096 f"'{cntr}' counter value (previous value: "
1097 f"{stats_snapshot[cntr][:, sw_if_index].sum()}, "
1098 f"expected diff: {diff})",
1099 )
Klement Sekera01578852023-01-26 13:14:01 +01001100 except IndexError as e:
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001101 # if diff is 0, then this most probably a case where
1102 # test declares multiple interfaces but traffic hasn't
1103 # passed through this one yet - which means the counter
1104 # value is 0 and can be ignored
1105 if 0 != diff:
Klement Sekera01578852023-01-26 13:14:01 +01001106 raise Exception(
1107 f"Couldn't sum counter: {cntr} on sw_if_index: {sw_if_index}"
1108 ) from e
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001109
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001110
1111def get_testcase_doc_name(test):
1112 return getdoc(test.__class__).splitlines()[0]
1113
1114
1115def get_test_description(descriptions, test):
1116 short_description = test.shortDescription()
1117 if descriptions and short_description:
1118 return short_description
1119 else:
1120 return str(test)
1121
1122
Dave Wallace8800f732023-08-31 00:47:44 -04001123def get_failed_testcase_linkname(failed_dir, testcase_dirname):
1124 return os.path.join(failed_dir, f"{testcase_dirname}-FAILED")
1125
1126
1127def get_testcase_dirname(testcase_class_name):
1128 return f"vpp-unittest-{testcase_class_name}"
1129
1130
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001131class TestCaseInfo(object):
1132 def __init__(self, logger, tempdir, vpp_pid, vpp_bin_path):
1133 self.logger = logger
1134 self.tempdir = tempdir
1135 self.vpp_pid = vpp_pid
1136 self.vpp_bin_path = vpp_bin_path
1137 self.core_crash_test = None
1138
1139
1140class VppTestResult(unittest.TestResult):
1141 """
1142 @property result_string
1143 String variable to store the test case result string.
1144 @property errors
1145 List variable containing 2-tuples of TestCase instances and strings
1146 holding formatted tracebacks. Each tuple represents a test which
1147 raised an unexpected exception.
1148 @property failures
1149 List variable containing 2-tuples of TestCase instances and strings
1150 holding formatted tracebacks. Each tuple represents a test where
1151 a failure was explicitly signalled using the TestCase.assert*()
1152 methods.
1153 """
1154
1155 failed_test_cases_info = set()
1156 core_crash_test_cases_info = set()
1157 current_test_case_info = None
1158
1159 def __init__(self, stream=None, descriptions=None, verbosity=None, runner=None):
1160 """
1161 :param stream File descriptor to store where to report test results.
1162 Set to the standard error stream by default.
1163 :param descriptions Boolean variable to store information if to use
1164 test case descriptions.
1165 :param verbosity Integer variable to store required verbosity level.
1166 """
1167 super(VppTestResult, self).__init__(stream, descriptions, verbosity)
1168 self.stream = stream
1169 self.descriptions = descriptions
1170 self.verbosity = verbosity
Klement Sekera47f35272023-03-29 16:04:58 +02001171 self.result_code = TestResultCode.TEST_RUN
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001172 self.result_string = None
1173 self.runner = runner
1174 self.printed = []
1175
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001176 def decodePcapFiles(self, test):
1177 if hasattr(test, "pg_interfaces") and len(test.pg_interfaces) > 0:
1178 testcase_dir = os.path.dirname(test.pg_interfaces[0].out_path)
1179 test.pg_interfaces[0].decode_pcap_files(
1180 testcase_dir, f"suite{test.__class__.__name__}"
1181 )
1182 test.pg_interfaces[0].decode_pcap_files(testcase_dir, test._testMethodName)
Dave Wallace8800f732023-08-31 00:47:44 -04001183
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001184 def addSuccess(self, test):
1185 """
1186 Record a test succeeded result
1187
1188 :param test:
1189
1190 """
Klement Sekera47f35272023-03-29 16:04:58 +02001191 self.log_result("addSuccess", test)
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001192 if "all" == config.decode_pcaps:
1193 self.decodePcapFiles(test)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001194 unittest.TestResult.addSuccess(self, test)
1195 self.result_string = colorize("OK", GREEN)
Klement Sekera47f35272023-03-29 16:04:58 +02001196 self.result_code = TestResultCode.PASS
1197 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001198
Klement Sekera47f35272023-03-29 16:04:58 +02001199 def addExpectedFailure(self, test, err):
1200 self.log_result("addExpectedFailure", test, err)
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001201 if "none" != config.decode_pcaps:
1202 self.decodePcapFiles(test)
Klement Sekera47f35272023-03-29 16:04:58 +02001203 super().addExpectedFailure(test, err)
1204 self.result_string = colorize("FAIL", GREEN)
1205 self.result_code = TestResultCode.EXPECTED_FAIL
1206 self.send_result_through_pipe(test, self.result_code)
1207
1208 def addUnexpectedSuccess(self, test):
1209 self.log_result("addUnexpectedSuccess", test)
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001210 if "none" != config.decode_pcaps:
1211 self.decodePcapFiles(test)
Klement Sekera47f35272023-03-29 16:04:58 +02001212 super().addUnexpectedSuccess(test)
1213 self.result_string = colorize("OK", RED)
1214 self.result_code = TestResultCode.UNEXPECTED_PASS
1215 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001216
1217 def addSkip(self, test, reason):
1218 """
1219 Record a test skipped.
1220
1221 :param test:
1222 :param reason:
1223
1224 """
Klement Sekera47f35272023-03-29 16:04:58 +02001225 self.log_result("addSkip", test, reason=reason)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001226 unittest.TestResult.addSkip(self, test, reason)
1227 self.result_string = colorize("SKIP", YELLOW)
1228
1229 if reason == "not enough cpus":
Klement Sekera47f35272023-03-29 16:04:58 +02001230 self.result_code = TestResultCode.SKIP_CPU_SHORTAGE
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001231 else:
Klement Sekera47f35272023-03-29 16:04:58 +02001232 self.result_code = TestResultCode.SKIP
1233 self.send_result_through_pipe(test, self.result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001234
1235 def symlink_failed(self):
1236 if self.current_test_case_info:
1237 try:
1238 failed_dir = config.failed_dir
Dave Wallace8800f732023-08-31 00:47:44 -04001239 link_path = get_failed_testcase_linkname(
1240 failed_dir, os.path.basename(self.current_test_case_info.tempdir)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001241 )
1242
1243 self.current_test_case_info.logger.debug(
1244 "creating a link to the failed test"
1245 )
1246 self.current_test_case_info.logger.debug(
1247 "os.symlink(%s, %s)"
1248 % (self.current_test_case_info.tempdir, link_path)
1249 )
1250 if os.path.exists(link_path):
1251 self.current_test_case_info.logger.debug("symlink already exists")
1252 else:
1253 os.symlink(self.current_test_case_info.tempdir, link_path)
1254
1255 except Exception as e:
1256 self.current_test_case_info.logger.error(e)
1257
1258 def send_result_through_pipe(self, test, result):
1259 if hasattr(self, "test_framework_result_pipe"):
1260 pipe = self.test_framework_result_pipe
1261 if pipe:
1262 pipe.send((test.id(), result))
1263
Klement Sekera47f35272023-03-29 16:04:58 +02001264 def log_result(self, fn, test, err=None, reason=None):
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001265 if self.current_test_case_info:
1266 if isinstance(test, unittest.suite._ErrorHolder):
1267 test_name = test.description
1268 else:
1269 test_name = "%s.%s(%s)" % (
1270 test.__class__.__name__,
1271 test._testMethodName,
1272 test._testMethodDoc,
1273 )
Klement Sekera47f35272023-03-29 16:04:58 +02001274 extra_msg = ""
1275 if err:
1276 extra_msg += f", error is {err}"
1277 if reason:
1278 extra_msg += f", reason is {reason}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001279 self.current_test_case_info.logger.debug(
Klement Sekera47f35272023-03-29 16:04:58 +02001280 f"--- {fn}() {test_name} called{extra_msg}"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001281 )
Klement Sekera47f35272023-03-29 16:04:58 +02001282 if err:
1283 self.current_test_case_info.logger.debug(
1284 "formatted exception is:\n%s" % "".join(format_exception(*err))
1285 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001286
Klement Sekera47f35272023-03-29 16:04:58 +02001287 def add_error(self, test, err, unittest_fn, result_code):
1288 self.result_code = result_code
1289 if result_code == TestResultCode.FAIL:
1290 self.log_result("addFailure", test, err=err)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001291 error_type_str = colorize("FAIL", RED)
Klement Sekera47f35272023-03-29 16:04:58 +02001292 elif result_code == TestResultCode.ERROR:
1293 self.log_result("addError", test, err=err)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001294 error_type_str = colorize("ERROR", RED)
1295 else:
Klement Sekera47f35272023-03-29 16:04:58 +02001296 raise Exception(f"Unexpected result code {result_code}")
Klement Sekeraca2f2e12024-05-23 11:19:51 +02001297
1298 if "none" != config.decode_pcaps:
1299 self.decodePcapFiles(test)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001300
1301 unittest_fn(self, test, err)
1302 if self.current_test_case_info:
1303 self.result_string = "%s [ temp dir used by test case: %s ]" % (
1304 error_type_str,
1305 self.current_test_case_info.tempdir,
1306 )
1307 self.symlink_failed()
1308 self.failed_test_cases_info.add(self.current_test_case_info)
1309 if is_core_present(self.current_test_case_info.tempdir):
1310 if not self.current_test_case_info.core_crash_test:
1311 if isinstance(test, unittest.suite._ErrorHolder):
1312 test_name = str(test)
1313 else:
1314 test_name = "'{!s}' ({!s})".format(
1315 get_testcase_doc_name(test), test.id()
1316 )
1317 self.current_test_case_info.core_crash_test = test_name
1318 self.core_crash_test_cases_info.add(self.current_test_case_info)
1319 else:
1320 self.result_string = "%s [no temp dir]" % error_type_str
1321
Klement Sekera47f35272023-03-29 16:04:58 +02001322 self.send_result_through_pipe(test, result_code)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001323
1324 def addFailure(self, test, err):
1325 """
1326 Record a test failed result
1327
1328 :param test:
1329 :param err: error message
1330
1331 """
Klement Sekera47f35272023-03-29 16:04:58 +02001332 self.add_error(test, err, unittest.TestResult.addFailure, TestResultCode.FAIL)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001333
1334 def addError(self, test, err):
1335 """
1336 Record a test error result
1337
1338 :param test:
1339 :param err: error message
1340
1341 """
Klement Sekera47f35272023-03-29 16:04:58 +02001342 self.add_error(test, err, unittest.TestResult.addError, TestResultCode.ERROR)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001343
1344 def getDescription(self, test):
1345 """
1346 Get test description
1347
1348 :param test:
1349 :returns: test description
1350
1351 """
1352 return get_test_description(self.descriptions, test)
1353
1354 def startTest(self, test):
1355 """
1356 Start a test
1357
1358 :param test:
1359
1360 """
1361
1362 def print_header(test):
1363 if test.__class__ in self.printed:
1364 return
1365
1366 test_doc = getdoc(test)
1367 if not test_doc:
1368 raise Exception("No doc string for test '%s'" % test.id())
1369
1370 test_title = test_doc.splitlines()[0].rstrip()
1371 test_title = colorize(test_title, GREEN)
1372 if test.is_tagged_run_solo():
1373 test_title = colorize(f"SOLO RUN: {test_title}", YELLOW)
1374
1375 # This block may overwrite the colorized title above,
1376 # but we want this to stand out and be fixed
1377 if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
1378 test_title = colorize(f"FIXME with VPP workers: {test_title}", RED)
1379
1380 if test.has_tag(TestCaseTag.FIXME_ASAN):
1381 test_title = colorize(f"FIXME with ASAN: {test_title}", RED)
1382 test.skip_fixme_asan()
1383
Andrew Yourtchenko9987d472024-07-02 18:02:09 +02001384 if (
1385 test.has_tag(TestCaseTag.FIXME_UBUNTU2204)
1386 and is_distro_ubuntu2204 == True
1387 ):
1388 test_title = colorize(f"FIXME with Ubuntu 22.04: {test_title}", RED)
1389 test.skip_fixme_ubuntu2204()
1390
Dave Wallace85ce9312024-08-19 18:47:55 -04001391 if (
1392 test.has_tag(TestCaseTag.FIXME_UBUNTU2404)
1393 and is_distro_ubuntu2404 == True
1394 ):
1395 test_title = colorize(f"FIXME with Ubuntu 24.04: {test_title}", RED)
1396 test.skip_fixme_ubuntu2404()
1397
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001398 if hasattr(test, "vpp_worker_count"):
1399 if test.vpp_worker_count == 0:
1400 test_title += " [main thread only]"
1401 elif test.vpp_worker_count == 1:
1402 test_title += " [1 worker thread]"
1403 else:
1404 test_title += f" [{test.vpp_worker_count} worker threads]"
1405
1406 if test.__class__.skipped_due_to_cpu_lack:
1407 test_title = colorize(
1408 f"{test_title} [skipped - not enough cpus, "
1409 f"required={test.__class__.get_cpus_required()}, "
1410 f"available={max_vpp_cpus}]",
1411 YELLOW,
1412 )
1413
1414 print(double_line_delim)
1415 print(test_title)
1416 print(double_line_delim)
1417 self.printed.append(test.__class__)
1418
1419 print_header(test)
1420 self.start_test = time.time()
1421 unittest.TestResult.startTest(self, test)
1422 if self.verbosity > 0:
1423 self.stream.writeln("Starting " + self.getDescription(test) + " ...")
1424 self.stream.writeln(single_line_delim)
1425
1426 def stopTest(self, test):
1427 """
1428 Called when the given test has been run
1429
1430 :param test:
1431
1432 """
1433 unittest.TestResult.stopTest(self, test)
1434
Klement Sekera47f35272023-03-29 16:04:58 +02001435 result_code_to_suffix = {
1436 TestResultCode.PASS: "",
1437 TestResultCode.FAIL: "",
1438 TestResultCode.ERROR: "",
1439 TestResultCode.SKIP: "",
1440 TestResultCode.TEST_RUN: "",
1441 TestResultCode.SKIP_CPU_SHORTAGE: "",
1442 TestResultCode.EXPECTED_FAIL: " [EXPECTED FAIL]",
1443 TestResultCode.UNEXPECTED_PASS: " [UNEXPECTED PASS]",
1444 }
1445
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001446 if self.verbosity > 0:
1447 self.stream.writeln(single_line_delim)
1448 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001449 "%-72s%s%s"
1450 % (
1451 self.getDescription(test),
1452 self.result_string,
1453 result_code_to_suffix[self.result_code],
1454 )
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001455 )
1456 self.stream.writeln(single_line_delim)
1457 else:
1458 self.stream.writeln(
Klement Sekera47f35272023-03-29 16:04:58 +02001459 "%-67s %4.2f %s%s"
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001460 % (
1461 self.getDescription(test),
1462 time.time() - self.start_test,
1463 self.result_string,
Klement Sekera47f35272023-03-29 16:04:58 +02001464 result_code_to_suffix[self.result_code],
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001465 )
1466 )
1467
Klement Sekera47f35272023-03-29 16:04:58 +02001468 self.send_result_through_pipe(test, TestResultCode.TEST_RUN)
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001469
1470 def printErrors(self):
1471 """
1472 Print errors from running the test case
1473 """
1474 if len(self.errors) > 0 or len(self.failures) > 0:
1475 self.stream.writeln()
1476 self.printErrorList("ERROR", self.errors)
1477 self.printErrorList("FAIL", self.failures)
1478
1479 # ^^ that is the last output from unittest before summary
1480 if not self.runner.print_summary:
1481 devnull = unittest.runner._WritelnDecorator(open(os.devnull, "w"))
1482 self.stream = devnull
1483 self.runner.stream = devnull
1484
1485 def printErrorList(self, flavour, errors):
1486 """
1487 Print error list to the output stream together with error type
1488 and test case description.
1489
1490 :param flavour: error type
1491 :param errors: iterable errors
1492
1493 """
1494 for test, err in errors:
1495 self.stream.writeln(double_line_delim)
1496 self.stream.writeln("%s: %s" % (flavour, self.getDescription(test)))
1497 self.stream.writeln(single_line_delim)
1498 self.stream.writeln("%s" % err)
1499
1500
1501class VppTestRunner(unittest.TextTestRunner):
1502 """
1503 A basic test runner implementation which prints results to standard error.
1504 """
1505
1506 @property
1507 def resultclass(self):
1508 """Class maintaining the results of the tests"""
1509 return VppTestResult
1510
1511 def __init__(
1512 self,
1513 keep_alive_pipe=None,
1514 descriptions=True,
1515 verbosity=1,
1516 result_pipe=None,
1517 failfast=False,
1518 buffer=False,
1519 resultclass=None,
1520 print_summary=True,
1521 **kwargs,
1522 ):
1523 # ignore stream setting here, use hard-coded stdout to be in sync
Dave Wallace8800f732023-08-31 00:47:44 -04001524 # with prints from VppAsfTestCase methods ...
Pratikshya Prasai657bdf72022-08-18 11:09:38 -04001525 super(VppTestRunner, self).__init__(
1526 sys.stdout, descriptions, verbosity, failfast, buffer, resultclass, **kwargs
1527 )
1528 KeepAliveReporter.pipe = keep_alive_pipe
1529
1530 self.orig_stream = self.stream
1531 self.resultclass.test_framework_result_pipe = result_pipe
1532
1533 self.print_summary = print_summary
1534
1535 def _makeResult(self):
1536 return self.resultclass(self.stream, self.descriptions, self.verbosity, self)
1537
1538 def run(self, test):
1539 """
1540 Run the tests
1541
1542 :param test:
1543
1544 """
1545 faulthandler.enable() # emit stack trace to stderr if killed by signal
1546
1547 result = super(VppTestRunner, self).run(test)
1548 if not self.print_summary:
1549 self.stream = self.orig_stream
1550 result.stream = self.orig_stream
1551 return result
1552
1553
1554class Worker(Thread):
1555 def __init__(self, executable_args, logger, env=None, *args, **kwargs):
1556 super(Worker, self).__init__(*args, **kwargs)
1557 self.logger = logger
1558 self.args = executable_args
1559 if hasattr(self, "testcase") and self.testcase.debug_all:
1560 if self.testcase.debug_gdbserver:
1561 self.args = [
1562 "/usr/bin/gdbserver",
1563 "localhost:{port}".format(port=self.testcase.gdbserver_port),
1564 ] + args
1565 elif self.testcase.debug_gdb and hasattr(self, "wait_for_gdb"):
1566 self.args.append(self.wait_for_gdb)
1567 self.app_bin = executable_args[0]
1568 self.app_name = os.path.basename(self.app_bin)
1569 if hasattr(self, "role"):
1570 self.app_name += " {role}".format(role=self.role)
1571 self.process = None
1572 self.result = None
1573 env = {} if env is None else env
1574 self.env = copy.deepcopy(env)
1575
1576 def wait_for_enter(self):
1577 if not hasattr(self, "testcase"):
1578 return
1579 if self.testcase.debug_all and self.testcase.debug_gdbserver:
1580 print()
1581 print(double_line_delim)
1582 print(
1583 "Spawned GDB Server for '{app}' with PID: {pid}".format(
1584 app=self.app_name, pid=self.process.pid
1585 )
1586 )
1587 elif self.testcase.debug_all and self.testcase.debug_gdb:
1588 print()
1589 print(double_line_delim)
1590 print(
1591 "Spawned '{app}' with PID: {pid}".format(
1592 app=self.app_name, pid=self.process.pid
1593 )
1594 )
1595 else:
1596 return
1597 print(single_line_delim)
1598 print("You can debug '{app}' using:".format(app=self.app_name))
1599 if self.testcase.debug_gdbserver:
1600 print(
1601 "sudo gdb "
1602 + self.app_bin
1603 + " -ex 'target remote localhost:{port}'".format(
1604 port=self.testcase.gdbserver_port
1605 )
1606 )
1607 print(
1608 "Now is the time to attach gdb by running the above "
1609 "command, set up breakpoints etc., then resume from "
1610 "within gdb by issuing the 'continue' command"
1611 )
1612 self.testcase.gdbserver_port += 1
1613 elif self.testcase.debug_gdb:
1614 print(
1615 "sudo gdb "
1616 + self.app_bin
1617 + " -ex 'attach {pid}'".format(pid=self.process.pid)
1618 )
1619 print(
1620 "Now is the time to attach gdb by running the above "
1621 "command and set up breakpoints etc., then resume from"
1622 " within gdb by issuing the 'continue' command"
1623 )
1624 print(single_line_delim)
1625 input("Press ENTER to continue running the testcase...")
1626
1627 def run(self):
1628 executable = self.args[0]
1629 if not os.path.exists(executable) or not os.access(
1630 executable, os.F_OK | os.X_OK
1631 ):
1632 # Exit code that means some system file did not exist,
1633 # could not be opened, or had some other kind of error.
1634 self.result = os.EX_OSFILE
1635 raise EnvironmentError(
1636 "executable '%s' is not found or executable." % executable
1637 )
1638 self.logger.debug(
1639 "Running executable '{app}': '{cmd}'".format(
1640 app=self.app_name, cmd=" ".join(self.args)
1641 )
1642 )
1643 env = os.environ.copy()
1644 env.update(self.env)
1645 env["CK_LOG_FILE_NAME"] = "-"
1646 self.process = subprocess.Popen(
1647 ["stdbuf", "-o0", "-e0"] + self.args,
1648 shell=False,
1649 env=env,
1650 preexec_fn=os.setpgrp,
1651 stdout=subprocess.PIPE,
1652 stderr=subprocess.PIPE,
1653 )
1654 self.wait_for_enter()
1655 out, err = self.process.communicate()
1656 self.logger.debug("Finished running `{app}'".format(app=self.app_name))
1657 self.logger.info("Return code is `%s'" % self.process.returncode)
1658 self.logger.info(single_line_delim)
1659 self.logger.info(
1660 "Executable `{app}' wrote to stdout:".format(app=self.app_name)
1661 )
1662 self.logger.info(single_line_delim)
1663 self.logger.info(out.decode("utf-8"))
1664 self.logger.info(single_line_delim)
1665 self.logger.info(
1666 "Executable `{app}' wrote to stderr:".format(app=self.app_name)
1667 )
1668 self.logger.info(single_line_delim)
1669 self.logger.info(err.decode("utf-8"))
1670 self.logger.info(single_line_delim)
1671 self.result = self.process.returncode
1672
1673
1674if __name__ == "__main__":
1675 pass