tests: cpus awareness
Introduce MAX_CPUS parameters to control maximum number of CPUs used by VPP(s) during testing, with default value 'auto' corresponding to all CPUs available. Calculate test CPU requirements by taking into account the number of workers, so a test requires 1 (main thread) + # of worker CPUs. When running tests, keep track of both running test jobs (controlled by TEST_JOBS parameter) and free CPUs. This then causes two limits in the system - to not exceed number of jobs in parallel but also to not exceed number of CPUs available. Skip tests which require more CPUs than are available in system (or more than MAX_CPUS) and print a warning message. Type: improvement Change-Id: Ib8fda54e4c6a36179d64160bb87fbd3a0011762d Signed-off-by: Klement Sekera <ksekera@cisco.com>
This commit is contained in:

committed by
Andrew Yourtchenko

parent
f70cf23376
commit
558ceabc6c
@ -16,11 +16,24 @@ from vpp_papi import VppEnum
|
|||||||
@tag_run_solo
|
@tag_run_solo
|
||||||
class TestMemif(VppTestCase):
|
class TestMemif(VppTestCase):
|
||||||
""" Memif Test Case """
|
""" Memif Test Case """
|
||||||
|
remote_class = RemoteVppTestCase
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_cpus_required(cls):
|
||||||
|
return (super().get_cpus_required() +
|
||||||
|
cls.remote_class.get_cpus_required())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def assign_cpus(cls, cpus):
|
||||||
|
remote_cpus = cpus[:cls.remote_class.get_cpus_required()]
|
||||||
|
my_cpus = cpus[cls.remote_class.get_cpus_required():]
|
||||||
|
cls.remote_class.assign_cpus(remote_cpus)
|
||||||
|
super().assign_cpus(my_cpus)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
# fork new process before client connects to VPP
|
# fork new process before client connects to VPP
|
||||||
cls.remote_test = RemoteClass(RemoteVppTestCase)
|
cls.remote_test = RemoteClass(cls.remote_class)
|
||||||
cls.remote_test.start_remote()
|
cls.remote_test.start_remote()
|
||||||
cls.remote_test.set_request_timeout(10)
|
cls.remote_test.set_request_timeout(10)
|
||||||
super(TestMemif, cls).setUpClass()
|
super(TestMemif, cls).setUpClass()
|
||||||
|
@ -366,7 +366,8 @@ help:
|
|||||||
@echo "Arguments controlling test runs:"
|
@echo "Arguments controlling test runs:"
|
||||||
@echo " V=[0|1|2] - set test verbosity level"
|
@echo " V=[0|1|2] - set test verbosity level"
|
||||||
@echo " 0=ERROR, 1=INFO, 2=DEBUG"
|
@echo " 0=ERROR, 1=INFO, 2=DEBUG"
|
||||||
@echo " TEST_JOBS=[<n>|auto] - use <n> parallel processes for test execution or automatic discovery of maximum acceptable processes (default: 1)"
|
@echo " TEST_JOBS=[<n>|auto] - use at most <n> parallel python processes for test execution, if auto, set to number of available cpus (default: 1)"
|
||||||
|
@echo " MAX_VPP_CPUS=[<n>|auto]- use at most <n> cpus for running vpp main and worker threads, if auto, set to number of available cpus (default: auto)"
|
||||||
@echo " CACHE_OUTPUT=[0|1] - cache VPP stdout/stderr and log as one block after test finishes (default: 1)"
|
@echo " CACHE_OUTPUT=[0|1] - cache VPP stdout/stderr and log as one block after test finishes (default: 1)"
|
||||||
@echo " FAILFAST=[0|1] - fail fast if 1, complete all tests if 0"
|
@echo " FAILFAST=[0|1] - fail fast if 1, complete all tests if 0"
|
||||||
@echo " TIMEOUT=<timeout> - fail test suite if any single test takes longer than <timeout> (in seconds) to finish (default: 600)"
|
@echo " TIMEOUT=<timeout> - fail test suite if any single test takes longer than <timeout> (in seconds) to finish (default: 600)"
|
||||||
|
21
test/cpu_config.py
Normal file
21
test/cpu_config.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
available_cpus = psutil.Process().cpu_affinity()
|
||||||
|
num_cpus = len(available_cpus)
|
||||||
|
|
||||||
|
max_vpp_cpus = os.getenv("MAX_VPP_CPUS", "auto").lower()
|
||||||
|
|
||||||
|
if max_vpp_cpus == "auto":
|
||||||
|
max_vpp_cpus = num_cpus
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
max_vpp_cpus = int(max_vpp_cpus)
|
||||||
|
except ValueError as e:
|
||||||
|
raise ValueError("Invalid MAX_VPP_CPUS value specified, valid "
|
||||||
|
"values are a positive integer or 'auto'") from e
|
||||||
|
if max_vpp_cpus <= 0:
|
||||||
|
raise ValueError("Invalid MAX_VPP_CPUS value specified, valid "
|
||||||
|
"values are a positive integer or 'auto'")
|
||||||
|
if max_vpp_cpus > num_cpus:
|
||||||
|
max_vpp_cpus = num_cpus
|
@ -22,6 +22,7 @@ from inspect import getdoc, isclass
|
|||||||
from traceback import format_exception
|
from traceback import format_exception
|
||||||
from logging import FileHandler, DEBUG, Formatter
|
from logging import FileHandler, DEBUG, Formatter
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
import scapy.compat
|
import scapy.compat
|
||||||
from scapy.packet import Raw
|
from scapy.packet import Raw
|
||||||
@ -42,6 +43,8 @@ from scapy.layers.inet import IPerror, TCPerror, UDPerror, ICMPerror
|
|||||||
from scapy.layers.inet6 import ICMPv6DestUnreach, ICMPv6EchoRequest
|
from scapy.layers.inet6 import ICMPv6DestUnreach, ICMPv6EchoRequest
|
||||||
from scapy.layers.inet6 import ICMPv6EchoReply
|
from scapy.layers.inet6 import ICMPv6EchoReply
|
||||||
|
|
||||||
|
from cpu_config import available_cpus, num_cpus, max_vpp_cpus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Set up an empty logger for the testcase that can be overridden as necessary
|
# Set up an empty logger for the testcase that can be overridden as necessary
|
||||||
@ -53,6 +56,7 @@ FAIL = 1
|
|||||||
ERROR = 2
|
ERROR = 2
|
||||||
SKIP = 3
|
SKIP = 3
|
||||||
TEST_RUN = 4
|
TEST_RUN = 4
|
||||||
|
SKIP_CPU_SHORTAGE = 5
|
||||||
|
|
||||||
|
|
||||||
class BoolEnvironmentVariable(object):
|
class BoolEnvironmentVariable(object):
|
||||||
@ -223,6 +227,21 @@ def _running_gcov_tests():
|
|||||||
running_gcov_tests = _running_gcov_tests()
|
running_gcov_tests = _running_gcov_tests()
|
||||||
|
|
||||||
|
|
||||||
|
def get_environ_vpp_worker_count():
|
||||||
|
worker_config = os.getenv("VPP_WORKER_CONFIG", None)
|
||||||
|
if worker_config:
|
||||||
|
elems = worker_config.split(" ")
|
||||||
|
if elems[0] != "workers" or len(elems) != 2:
|
||||||
|
raise ValueError("Wrong VPP_WORKER_CONFIG == '%s' value." %
|
||||||
|
worker_config)
|
||||||
|
return int(elems[1])
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
environ_vpp_worker_count = get_environ_vpp_worker_count()
|
||||||
|
|
||||||
|
|
||||||
class KeepAliveReporter(object):
|
class KeepAliveReporter(object):
|
||||||
"""
|
"""
|
||||||
Singleton object which reports test start to parent process
|
Singleton object which reports test start to parent process
|
||||||
@ -292,7 +311,21 @@ class DummyVpp:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class VppTestCase(unittest.TestCase):
|
class CPUInterface(ABC):
|
||||||
|
cpus = []
|
||||||
|
skipped_due_to_cpu_lack = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def get_cpus_required(cls):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def assign_cpus(cls, cpus):
|
||||||
|
cls.cpus = cpus
|
||||||
|
|
||||||
|
|
||||||
|
class VppTestCase(CPUInterface, unittest.TestCase):
|
||||||
"""This subclass is a base class for VPP test cases that are implemented as
|
"""This subclass is a base class for VPP test cases that are implemented as
|
||||||
classes. It provides methods to create and run test case.
|
classes. It provides methods to create and run test case.
|
||||||
"""
|
"""
|
||||||
@ -358,34 +391,18 @@ class VppTestCase(unittest.TestCase):
|
|||||||
if dl == "gdb-all" or dl == "gdbserver-all":
|
if dl == "gdb-all" or dl == "gdbserver-all":
|
||||||
cls.debug_all = True
|
cls.debug_all = True
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def get_least_used_cpu():
|
def get_vpp_worker_count(cls):
|
||||||
cpu_usage_list = [set(range(psutil.cpu_count()))]
|
if not hasattr(cls, "vpp_worker_count"):
|
||||||
vpp_processes = [p for p in psutil.process_iter(attrs=['pid', 'name'])
|
if cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
|
||||||
if 'vpp_main' == p.info['name']]
|
cls.vpp_worker_count = 0
|
||||||
for vpp_process in vpp_processes:
|
|
||||||
for cpu_usage_set in cpu_usage_list:
|
|
||||||
try:
|
|
||||||
cpu_num = vpp_process.cpu_num()
|
|
||||||
if cpu_num in cpu_usage_set:
|
|
||||||
cpu_usage_set_index = cpu_usage_list.index(
|
|
||||||
cpu_usage_set)
|
|
||||||
if cpu_usage_set_index == len(cpu_usage_list) - 1:
|
|
||||||
cpu_usage_list.append({cpu_num})
|
|
||||||
else:
|
else:
|
||||||
cpu_usage_list[cpu_usage_set_index + 1].add(
|
cls.vpp_worker_count = environ_vpp_worker_count
|
||||||
cpu_num)
|
return cls.vpp_worker_count
|
||||||
cpu_usage_set.remove(cpu_num)
|
|
||||||
break
|
|
||||||
except psutil.NoSuchProcess:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for cpu_usage_set in cpu_usage_list:
|
@classmethod
|
||||||
if len(cpu_usage_set) > 0:
|
def get_cpus_required(cls):
|
||||||
min_usage_set = cpu_usage_set
|
return 1 + cls.get_vpp_worker_count()
|
||||||
break
|
|
||||||
|
|
||||||
return random.choice(tuple(min_usage_set))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpConstants(cls):
|
def setUpConstants(cls):
|
||||||
@ -417,20 +434,6 @@ class VppTestCase(unittest.TestCase):
|
|||||||
if coredump_size is None:
|
if coredump_size is None:
|
||||||
coredump_size = "coredump-size unlimited"
|
coredump_size = "coredump-size unlimited"
|
||||||
|
|
||||||
cpu_core_number = cls.get_least_used_cpu()
|
|
||||||
if not hasattr(cls, "vpp_worker_count"):
|
|
||||||
cls.vpp_worker_count = 0
|
|
||||||
worker_config = os.getenv("VPP_WORKER_CONFIG", "")
|
|
||||||
if worker_config:
|
|
||||||
elems = worker_config.split(" ")
|
|
||||||
if elems[0] != "workers" or len(elems) != 2:
|
|
||||||
raise ValueError("Wrong VPP_WORKER_CONFIG == '%s' value." %
|
|
||||||
worker_config)
|
|
||||||
cls.vpp_worker_count = int(elems[1])
|
|
||||||
if cls.vpp_worker_count > 0 and\
|
|
||||||
cls.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
|
|
||||||
cls.vpp_worker_count = 0
|
|
||||||
|
|
||||||
default_variant = os.getenv("VARIANT")
|
default_variant = os.getenv("VARIANT")
|
||||||
if default_variant is not None:
|
if default_variant is not None:
|
||||||
default_variant = "defaults { %s 100 }" % default_variant
|
default_variant = "defaults { %s 100 }" % default_variant
|
||||||
@ -447,9 +450,10 @@ class VppTestCase(unittest.TestCase):
|
|||||||
coredump_size, "runtime-dir", cls.tempdir, "}",
|
coredump_size, "runtime-dir", cls.tempdir, "}",
|
||||||
"api-trace", "{", "on", "}",
|
"api-trace", "{", "on", "}",
|
||||||
"api-segment", "{", "prefix", cls.get_api_segment_prefix(), "}",
|
"api-segment", "{", "prefix", cls.get_api_segment_prefix(), "}",
|
||||||
"cpu", "{", "main-core", str(cpu_core_number), ]
|
"cpu", "{", "main-core", str(cls.cpus[0]), ]
|
||||||
if cls.vpp_worker_count:
|
if cls.get_vpp_worker_count():
|
||||||
cls.vpp_cmdline.extend(["workers", str(cls.vpp_worker_count)])
|
cls.vpp_cmdline.extend([
|
||||||
|
"corelist-workers", ",".join([str(x) for x in cls.cpus[1:]])])
|
||||||
cls.vpp_cmdline.extend([
|
cls.vpp_cmdline.extend([
|
||||||
"}",
|
"}",
|
||||||
"physmem", "{", "max-size", "32m", "}",
|
"physmem", "{", "max-size", "32m", "}",
|
||||||
@ -509,11 +513,12 @@ class VppTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run_vpp(cls):
|
def run_vpp(cls):
|
||||||
|
cls.logger.debug(f"Assigned cpus: {cls.cpus}")
|
||||||
cmdline = cls.vpp_cmdline
|
cmdline = cls.vpp_cmdline
|
||||||
|
|
||||||
if cls.debug_gdbserver:
|
if cls.debug_gdbserver:
|
||||||
gdbserver = '/usr/bin/gdbserver'
|
gdbserver = '/usr/bin/gdbserver'
|
||||||
if not os.path.isfile(gdbserver) or \
|
if not os.path.isfile(gdbserver) or\
|
||||||
not os.access(gdbserver, os.X_OK):
|
not os.access(gdbserver, os.X_OK):
|
||||||
raise Exception("gdbserver binary '%s' does not exist or is "
|
raise Exception("gdbserver binary '%s' does not exist or is "
|
||||||
"not executable" % gdbserver)
|
"not executable" % gdbserver)
|
||||||
@ -1349,6 +1354,7 @@ class VppTestResult(unittest.TestResult):
|
|||||||
self.verbosity = verbosity
|
self.verbosity = verbosity
|
||||||
self.result_string = None
|
self.result_string = None
|
||||||
self.runner = runner
|
self.runner = runner
|
||||||
|
self.printed = []
|
||||||
|
|
||||||
def addSuccess(self, test):
|
def addSuccess(self, test):
|
||||||
"""
|
"""
|
||||||
@ -1383,6 +1389,9 @@ class VppTestResult(unittest.TestResult):
|
|||||||
unittest.TestResult.addSkip(self, test, reason)
|
unittest.TestResult.addSkip(self, test, reason)
|
||||||
self.result_string = colorize("SKIP", YELLOW)
|
self.result_string = colorize("SKIP", YELLOW)
|
||||||
|
|
||||||
|
if reason == "not enough cpus":
|
||||||
|
self.send_result_through_pipe(test, SKIP_CPU_SHORTAGE)
|
||||||
|
else:
|
||||||
self.send_result_through_pipe(test, SKIP)
|
self.send_result_through_pipe(test, SKIP)
|
||||||
|
|
||||||
def symlink_failed(self):
|
def symlink_failed(self):
|
||||||
@ -1501,28 +1510,34 @@ class VppTestResult(unittest.TestResult):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def print_header(test):
|
def print_header(test):
|
||||||
|
if test.__class__ in self.printed:
|
||||||
|
return
|
||||||
|
|
||||||
test_doc = getdoc(test)
|
test_doc = getdoc(test)
|
||||||
if not test_doc:
|
if not test_doc:
|
||||||
raise Exception("No doc string for test '%s'" % test.id())
|
raise Exception("No doc string for test '%s'" % test.id())
|
||||||
|
|
||||||
test_title = test_doc.splitlines()[0]
|
test_title = test_doc.splitlines()[0]
|
||||||
test_title_colored = colorize(test_title, GREEN)
|
test_title = colorize(test_title, GREEN)
|
||||||
if test.is_tagged_run_solo():
|
if test.is_tagged_run_solo():
|
||||||
# long live PEP-8 and 80 char width limitation...
|
test_title = colorize(f"SOLO RUN: {test_title}", YELLOW)
|
||||||
c = YELLOW
|
|
||||||
test_title_colored = colorize("SOLO RUN: " + test_title, c)
|
|
||||||
|
|
||||||
# This block may overwrite the colorized title above,
|
# This block may overwrite the colorized title above,
|
||||||
# but we want this to stand out and be fixed
|
# but we want this to stand out and be fixed
|
||||||
if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
|
if test.has_tag(TestCaseTag.FIXME_VPP_WORKERS):
|
||||||
c = RED
|
test_title = colorize(
|
||||||
w = "FIXME with VPP workers: "
|
f"FIXME with VPP workers: {test_title}", RED)
|
||||||
test_title_colored = colorize(w + test_title, c)
|
|
||||||
|
if test.__class__.skipped_due_to_cpu_lack:
|
||||||
|
test_title = colorize(
|
||||||
|
f"{test_title} [skipped - not enough cpus, "
|
||||||
|
f"required={test.__class__.get_cpus_required()}, "
|
||||||
|
f"available={max_vpp_cpus}]", YELLOW)
|
||||||
|
|
||||||
if not hasattr(test.__class__, '_header_printed'):
|
|
||||||
print(double_line_delim)
|
print(double_line_delim)
|
||||||
print(test_title_colored)
|
print(test_title)
|
||||||
print(double_line_delim)
|
print(double_line_delim)
|
||||||
test.__class__._header_printed = True
|
self.printed.append(test.__class__)
|
||||||
|
|
||||||
print_header(test)
|
print_header(test)
|
||||||
self.start_test = time.time()
|
self.start_test = time.time()
|
||||||
|
@ -11,7 +11,7 @@ single_line_delim = '-' * 78
|
|||||||
|
|
||||||
|
|
||||||
def colorize(msg, color):
|
def colorize(msg, color):
|
||||||
return color + msg + COLOR_RESET
|
return f"{color}{msg}{COLOR_RESET}"
|
||||||
|
|
||||||
|
|
||||||
class ColorFormatter(logging.Formatter):
|
class ColorFormatter(logging.Formatter):
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@ from framework import VppDiedError, VppTestCase, KeepAliveReporter
|
|||||||
|
|
||||||
class SanityTestCase(VppTestCase):
|
class SanityTestCase(VppTestCase):
|
||||||
""" Sanity test case - verify whether VPP is able to start """
|
""" Sanity test case - verify whether VPP is able to start """
|
||||||
pass
|
cpus = [0]
|
||||||
|
|
||||||
# don't ask to debug SanityTestCase
|
# don't ask to debug SanityTestCase
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
"""Test framework utility functions tests"""
|
"""Test framework utility functions tests"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
from framework import VppTestRunner
|
from framework import VppTestRunner, CPUInterface
|
||||||
from vpp_papi import mac_pton, mac_ntop
|
from vpp_papi import mac_pton, mac_ntop
|
||||||
|
|
||||||
|
|
||||||
class TestUtil (unittest.TestCase):
|
class TestUtil (CPUInterface, unittest.TestCase):
|
||||||
""" Test framework utility tests """
|
""" Test framework utility tests """
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -23,6 +23,10 @@ class TestUtil (unittest.TestCase):
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_cpus_required(cls):
|
||||||
|
return 0
|
||||||
|
|
||||||
def test_mac_to_binary(self):
|
def test_mac_to_binary(self):
|
||||||
""" MAC to binary and back """
|
""" MAC to binary and back """
|
||||||
mac = 'aa:bb:cc:dd:ee:ff'
|
mac = 'aa:bb:cc:dd:ee:ff'
|
||||||
|
Reference in New Issue
Block a user