mirror of
https://git.proxmox.com/git/mirror_frr
synced 2025-08-05 16:57:44 +00:00
Merge pull request #13618 from LabNConsulting/chopps/fixlogging
This commit is contained in:
commit
6a771192de
@ -4,6 +4,7 @@ Topotest conftest.py file.
|
||||
"""
|
||||
# pylint: disable=consider-using-f-string
|
||||
|
||||
import contextlib
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
@ -12,6 +13,7 @@ import resource
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import lib.fixtures
|
||||
import pytest
|
||||
@ -41,6 +43,30 @@ except (AttributeError, ImportError):
|
||||
pass
|
||||
|
||||
|
||||
# Remove this and use munet version when we move to pytest_asyncio
|
||||
@contextlib.contextmanager
|
||||
def chdir(ndir, desc=""):
|
||||
odir = os.getcwd()
|
||||
os.chdir(ndir)
|
||||
if desc:
|
||||
logging.debug("%s: chdir from %s to %s", desc, odir, ndir)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if desc:
|
||||
logging.debug("%s: chdir back from %s to %s", desc, ndir, odir)
|
||||
os.chdir(odir)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def log_handler(basename, logpath):
|
||||
topolog.logstart(basename, logpath)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
topolog.logfinish(basename, logpath)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""
|
||||
Add topology-only option to the topology tester. This option makes pytest
|
||||
@ -271,6 +297,20 @@ def check_for_memleaks():
|
||||
pytest.fail("memleaks found for daemons: " + " ".join(daemons))
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def module_autouse(request):
|
||||
basename = get_test_logdir(request.node.nodeid, True)
|
||||
logdir = Path(topotest.g_pytest_config.option.rundir) / basename
|
||||
logpath = logdir / "exec.log"
|
||||
|
||||
subprocess.check_call("mkdir -p -m 1777 {}".format(logdir), shell=True)
|
||||
|
||||
with log_handler(basename, logpath):
|
||||
sdir = os.path.dirname(os.path.realpath(request.fspath))
|
||||
with chdir(sdir, "module autouse fixture"):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True, scope="module")
|
||||
def module_check_memtest(request):
|
||||
yield
|
||||
@ -282,14 +322,19 @@ def module_check_memtest(request):
|
||||
check_for_memleaks()
|
||||
|
||||
|
||||
def pytest_runtest_logstart(nodeid, location):
|
||||
# location is (filename, lineno, testname)
|
||||
topolog.logstart(nodeid, location, topotest.g_pytest_config.option.rundir)
|
||||
|
||||
|
||||
def pytest_runtest_logfinish(nodeid, location):
|
||||
# location is (filename, lineno, testname)
|
||||
topolog.logfinish(nodeid, location)
|
||||
#
|
||||
# Disable per test function logging as FRR CI system can't handle it.
|
||||
#
|
||||
# @pytest.fixture(autouse=True, scope="function")
|
||||
# def function_autouse(request):
|
||||
# # For tests we actually use the logdir name as the logfile base
|
||||
# logbase = get_test_logdir(nodeid=request.node.nodeid, module=False)
|
||||
# logbase = os.path.join(topotest.g_pytest_config.option.rundir, logbase)
|
||||
# logpath = Path(logbase)
|
||||
# path = Path(f"{logpath.parent}/exec-{logpath.name}.log")
|
||||
# subprocess.check_call("mkdir -p -m 1777 {}".format(logpath.parent), shell=True)
|
||||
# with log_handler(request.node.nodeid, path):
|
||||
# yield
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@ -340,8 +385,10 @@ def pytest_configure(config):
|
||||
os.environ["PYTEST_TOPOTEST_WORKER"] = ""
|
||||
is_xdist = os.environ["PYTEST_XDIST_MODE"] != "no"
|
||||
is_worker = False
|
||||
wname = ""
|
||||
else:
|
||||
os.environ["PYTEST_TOPOTEST_WORKER"] = os.environ["PYTEST_XDIST_WORKER"]
|
||||
wname = os.environ["PYTEST_XDIST_WORKER"]
|
||||
os.environ["PYTEST_TOPOTEST_WORKER"] = wname
|
||||
is_xdist = True
|
||||
is_worker = True
|
||||
|
||||
@ -375,6 +422,16 @@ def pytest_configure(config):
|
||||
if not config.getoption("--log-file") and not config.getini("log_file"):
|
||||
config.option.log_file = os.path.join(rundir, "exec.log")
|
||||
|
||||
# Handle pytest-xdist each worker get's it's own top level log file
|
||||
# `exec-worker-N.log`
|
||||
if wname:
|
||||
wname = wname.replace("gw", "worker-")
|
||||
cpath = Path(config.option.log_file).absolute()
|
||||
config.option.log_file = f"{cpath.parent}/{cpath.stem}-{wname}{cpath.suffix}"
|
||||
elif is_xdist:
|
||||
cpath = Path(config.option.log_file).absolute()
|
||||
config.option.log_file = f"{cpath.parent}/{cpath.stem}-xdist{cpath.suffix}"
|
||||
|
||||
# Turn on live logging if user specified verbose and the config has a CLI level set
|
||||
if config.getoption("--verbose") and not is_xdist and not config.getini("log_cli"):
|
||||
if config.getoption("--log-cli-level", None) is None:
|
||||
@ -433,6 +490,10 @@ def pytest_configure(config):
|
||||
|
||||
@pytest.fixture(autouse=True, scope="session")
|
||||
def setup_session_auto():
|
||||
# Aligns logs nicely
|
||||
logging.addLevelName(logging.WARNING, " WARN")
|
||||
logging.addLevelName(logging.INFO, " INFO")
|
||||
|
||||
if "PYTEST_TOPOTEST_WORKER" not in os.environ:
|
||||
is_worker = False
|
||||
elif not os.environ["PYTEST_TOPOTEST_WORKER"]:
|
||||
|
@ -121,7 +121,7 @@ class Mininet(BaseMunet):
|
||||
|
||||
g_mnet_inst = None
|
||||
|
||||
def __init__(self, rundir=None, pytestconfig=None):
|
||||
def __init__(self, rundir=None, pytestconfig=None, logger=None):
|
||||
"""
|
||||
Create a Micronet.
|
||||
"""
|
||||
@ -140,7 +140,7 @@ class Mininet(BaseMunet):
|
||||
# os.umask(0)
|
||||
|
||||
super(Mininet, self).__init__(
|
||||
pid=False, rundir=rundir, pytestconfig=pytestconfig
|
||||
pid=False, rundir=rundir, pytestconfig=pytestconfig, logger=logger
|
||||
)
|
||||
|
||||
# From munet/munet/native.py
|
||||
|
@ -84,7 +84,7 @@ def get_exabgp_cmd(commander=None):
|
||||
"""Return the command to use for ExaBGP version < 4."""
|
||||
|
||||
if commander is None:
|
||||
commander = Commander("topogen")
|
||||
commander = Commander("exabgp", logger=logging.getLogger("exabgp"))
|
||||
|
||||
def exacmd_version_ok(exacmd):
|
||||
logger.debug("checking %s for exabgp < version 4", exacmd)
|
||||
@ -107,7 +107,7 @@ def get_exabgp_cmd(commander=None):
|
||||
exacmd = py2_path + " -m exabgp"
|
||||
if exacmd_version_ok(exacmd):
|
||||
return exacmd
|
||||
py2_path = commander.get_exec_path("python")
|
||||
py2_path = commander.get_exec_path("python")
|
||||
if py2_path:
|
||||
exacmd = py2_path + " -m exabgp"
|
||||
if exacmd_version_ok(exacmd):
|
||||
@ -209,7 +209,11 @@ class Topogen(object):
|
||||
# Mininet(Micronet) to build the actual topology.
|
||||
assert not inspect.isclass(topodef)
|
||||
|
||||
self.net = Mininet(rundir=self.logdir, pytestconfig=topotest.g_pytest_config)
|
||||
self.net = Mininet(
|
||||
rundir=self.logdir,
|
||||
pytestconfig=topotest.g_pytest_config,
|
||||
logger=topolog.get_logger("mu", log_level="debug"),
|
||||
)
|
||||
|
||||
# Adjust the parent namespace
|
||||
topotest.fix_netns_limits(self.net)
|
||||
@ -1090,8 +1094,9 @@ class TopoSwitch(TopoGear):
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
def __init__(self, tgen, name, **params):
|
||||
logger = topolog.get_logger(name, log_level="debug")
|
||||
super(TopoSwitch, self).__init__(tgen, name, **params)
|
||||
tgen.net.add_switch(name)
|
||||
tgen.net.add_switch(name, logger=logger)
|
||||
|
||||
def __str__(self):
|
||||
gear = super(TopoSwitch, self).__str__()
|
||||
|
@ -15,13 +15,6 @@ This file defines our logging abstraction.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if sys.version_info[0] > 2:
|
||||
pass
|
||||
else:
|
||||
pass
|
||||
|
||||
try:
|
||||
from xdist import is_xdist_controller
|
||||
@ -31,8 +24,6 @@ except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
BASENAME = "topolog"
|
||||
|
||||
# Helper dictionary to convert Topogen logging levels to Python's logging.
|
||||
DEBUG_TOPO2LOGGING = {
|
||||
"debug": logging.DEBUG,
|
||||
@ -42,13 +33,43 @@ DEBUG_TOPO2LOGGING = {
|
||||
"error": logging.ERROR,
|
||||
"critical": logging.CRITICAL,
|
||||
}
|
||||
FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s: %(name)s: %(message)s"
|
||||
FORMAT = "%(asctime)s %(levelname)s: %(name)s: %(message)s"
|
||||
|
||||
handlers = {}
|
||||
logger = logging.getLogger("topolog")
|
||||
logger = logging.getLogger("topo")
|
||||
|
||||
|
||||
def set_handler(l, target=None):
|
||||
# Remove this and use munet version when we move to pytest_asyncio
|
||||
def get_test_logdir(nodeid=None, module=False):
|
||||
"""Get log directory relative pathname."""
|
||||
xdist_worker = os.getenv("PYTEST_XDIST_WORKER", "")
|
||||
mode = os.getenv("PYTEST_XDIST_MODE", "no")
|
||||
|
||||
# nodeid: all_protocol_startup/test_all_protocol_startup.py::test_router_running
|
||||
# may be missing "::testname" if module is True
|
||||
if not nodeid:
|
||||
nodeid = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0]
|
||||
|
||||
cur_test = nodeid.replace("[", "_").replace("]", "_")
|
||||
if module:
|
||||
idx = cur_test.rfind("::")
|
||||
path = cur_test if idx == -1 else cur_test[:idx]
|
||||
testname = ""
|
||||
else:
|
||||
path, testname = cur_test.split("::")
|
||||
testname = testname.replace("/", ".")
|
||||
path = path[:-3].replace("/", ".")
|
||||
|
||||
# We use different logdir paths based on how xdist is running.
|
||||
if mode == "each":
|
||||
if module:
|
||||
return os.path.join(path, "worker-logs", xdist_worker)
|
||||
return os.path.join(path, testname, xdist_worker)
|
||||
assert mode in ("no", "load", "loadfile", "loadscope"), f"Unknown dist mode {mode}"
|
||||
return path if module else os.path.join(path, testname)
|
||||
|
||||
|
||||
def set_handler(lg, target=None):
|
||||
if target is None:
|
||||
h = logging.NullHandler()
|
||||
else:
|
||||
@ -59,106 +80,81 @@ def set_handler(l, target=None):
|
||||
h.setFormatter(logging.Formatter(fmt=FORMAT))
|
||||
# Don't filter anything at the handler level
|
||||
h.setLevel(logging.DEBUG)
|
||||
l.addHandler(h)
|
||||
lg.addHandler(h)
|
||||
return h
|
||||
|
||||
|
||||
def set_log_level(l, level):
|
||||
def set_log_level(lg, level):
|
||||
"Set the logging level."
|
||||
# Messages sent to this logger only are created if this level or above.
|
||||
log_level = DEBUG_TOPO2LOGGING.get(level, level)
|
||||
l.setLevel(log_level)
|
||||
lg.setLevel(log_level)
|
||||
|
||||
|
||||
def get_logger(name, log_level=None, target=None):
|
||||
l = logging.getLogger("{}.{}".format(BASENAME, name))
|
||||
def reset_logger(lg):
|
||||
while lg.handlers:
|
||||
x = lg.handlers.pop()
|
||||
x.close()
|
||||
lg.removeHandler(x)
|
||||
|
||||
|
||||
def get_logger(name, log_level=None, target=None, reset=True):
|
||||
lg = logging.getLogger(name)
|
||||
|
||||
if reset:
|
||||
reset_logger(lg)
|
||||
|
||||
if log_level is not None:
|
||||
set_log_level(l, log_level)
|
||||
set_log_level(lg, log_level)
|
||||
|
||||
if target is not None:
|
||||
set_handler(l, target)
|
||||
set_handler(lg, target)
|
||||
|
||||
return l
|
||||
return lg
|
||||
|
||||
|
||||
# nodeid: all_protocol_startup/test_all_protocol_startup.py::test_router_running
|
||||
|
||||
|
||||
def get_test_logdir(nodeid=None):
|
||||
"""Get log directory relative pathname."""
|
||||
xdist_worker = os.getenv("PYTEST_XDIST_WORKER", "")
|
||||
mode = os.getenv("PYTEST_XDIST_MODE", "no")
|
||||
|
||||
if not nodeid:
|
||||
nodeid = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0]
|
||||
|
||||
cur_test = nodeid.replace("[", "_").replace("]", "_")
|
||||
path, testname = cur_test.split("::")
|
||||
path = path[:-3].replace("/", ".")
|
||||
|
||||
# We use different logdir paths based on how xdist is running.
|
||||
if mode == "each":
|
||||
return os.path.join(path, testname, xdist_worker)
|
||||
elif mode == "load":
|
||||
return os.path.join(path, testname)
|
||||
else:
|
||||
assert (
|
||||
mode == "no" or mode == "loadfile" or mode == "loadscope"
|
||||
), "Unknown dist mode {}".format(mode)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def logstart(nodeid, location, rundir):
|
||||
def logstart(nodeid, logpath):
|
||||
"""Called from pytest before module setup."""
|
||||
|
||||
mode = os.getenv("PYTEST_XDIST_MODE", "no")
|
||||
worker = os.getenv("PYTEST_TOPOTEST_WORKER", "")
|
||||
|
||||
# We only per-test log in the workers (or non-dist)
|
||||
if not worker and mode != "no":
|
||||
return
|
||||
|
||||
wstr = f" on worker {worker}" if worker else ""
|
||||
handler_id = nodeid + worker
|
||||
assert handler_id not in handlers
|
||||
logpath = logpath.absolute()
|
||||
|
||||
rel_log_dir = get_test_logdir(nodeid)
|
||||
exec_log_dir = os.path.join(rundir, rel_log_dir)
|
||||
subprocess.check_call(
|
||||
"mkdir -p {0} && chmod 1777 {0}".format(exec_log_dir), shell=True
|
||||
)
|
||||
exec_log_path = os.path.join(exec_log_dir, "exec.log")
|
||||
logging.debug("logstart: adding logging for %s%s at %s", nodeid, wstr, logpath)
|
||||
root_logger = logging.getLogger()
|
||||
handler = logging.FileHandler(logpath, mode="w")
|
||||
handler.setFormatter(logging.Formatter(FORMAT))
|
||||
|
||||
# Add test based exec log handler
|
||||
h = set_handler(logger, exec_log_path)
|
||||
handlers[handler_id] = h
|
||||
root_logger.addHandler(handler)
|
||||
handlers[handler_id] = handler
|
||||
|
||||
if worker:
|
||||
logger.info(
|
||||
"Logging on worker %s for %s into %s", worker, handler_id, exec_log_path
|
||||
)
|
||||
else:
|
||||
logger.info("Logging for %s into %s", handler_id, exec_log_path)
|
||||
logging.debug("logstart: added logging for %s%s at %s", nodeid, wstr, logpath)
|
||||
return handler
|
||||
|
||||
|
||||
def logfinish(nodeid, location):
|
||||
def logfinish(nodeid, logpath):
|
||||
"""Called from pytest after module teardown."""
|
||||
# This function may not be called if pytest is interrupted.
|
||||
|
||||
worker = os.getenv("PYTEST_TOPOTEST_WORKER", "")
|
||||
wstr = f" on worker {worker}" if worker else ""
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
handler_id = nodeid + worker
|
||||
|
||||
if handler_id in handlers:
|
||||
# Remove test based exec log handler
|
||||
if worker:
|
||||
logger.info("Closing logs for %s", handler_id)
|
||||
|
||||
if handler_id not in handlers:
|
||||
logging.critical("can't find log handler to remove")
|
||||
else:
|
||||
logging.debug(
|
||||
"logfinish: removing logging for %s%s at %s", nodeid, wstr, logpath
|
||||
)
|
||||
h = handlers[handler_id]
|
||||
logger.removeHandler(handlers[handler_id])
|
||||
root_logger.removeHandler(h)
|
||||
h.flush()
|
||||
h.close()
|
||||
del handlers[handler_id]
|
||||
logging.debug(
|
||||
"logfinish: removed logging for %s%s at %s", nodeid, wstr, logpath
|
||||
)
|
||||
|
||||
|
||||
console_handler = set_handler(logger, None)
|
||||
|
@ -24,6 +24,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import logging
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
|
||||
@ -38,7 +39,7 @@ g_pytest_config = None
|
||||
|
||||
|
||||
def get_logs_path(rundir):
|
||||
logspath = topolog.get_test_logdir()
|
||||
logspath = topolog.get_test_logdir(module=True)
|
||||
return os.path.join(rundir, logspath)
|
||||
|
||||
|
||||
@ -1137,7 +1138,9 @@ def _sysctl_assure(commander, variable, value):
|
||||
def sysctl_atleast(commander, variable, min_value, raises=False):
|
||||
try:
|
||||
if commander is None:
|
||||
commander = micronet.Commander("topotest")
|
||||
logger = logging.getLogger("topotest")
|
||||
commander = micronet.Commander("sysctl", logger=logger)
|
||||
|
||||
return _sysctl_atleast(commander, variable, min_value)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.warning(
|
||||
@ -1153,7 +1156,8 @@ def sysctl_atleast(commander, variable, min_value, raises=False):
|
||||
def sysctl_assure(commander, variable, value, raises=False):
|
||||
try:
|
||||
if commander is None:
|
||||
commander = micronet.Commander("topotest")
|
||||
logger = logging.getLogger("topotest")
|
||||
commander = micronet.Commander("sysctl", logger=logger)
|
||||
return _sysctl_assure(commander, variable, value)
|
||||
except subprocess.CalledProcessError as error:
|
||||
logger.warning(
|
||||
|
Loading…
Reference in New Issue
Block a user