diff --git a/doc/developer/topotests.rst b/doc/developer/topotests.rst index 33ab6c7c92..7b1fde7b53 100644 --- a/doc/developer/topotests.rst +++ b/doc/developer/topotests.rst @@ -310,32 +310,6 @@ Test will set exit code which can be used with ``git bisect``. For the simulated topology, see the description in the python file. -StdErr log from daemos after exit -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -To enable the reporting of any messages seen on StdErr after the daemons exit, -the following env variable can be set:: - - export TOPOTESTS_CHECK_STDERR=Yes - -(The value doesn't matter at this time. The check is whether the env -variable exists or not.) There is no pass/fail on this reporting; the -Output will be reported to the console. - -Collect Memory Leak Information -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -FRR processes can report unfreed memory allocations upon exit. To -enable the reporting of memory leaks, define an environment variable -``TOPOTESTS_CHECK_MEMLEAK`` with the file prefix, i.e.:: - - export TOPOTESTS_CHECK_MEMLEAK="/home/mydir/memleak_" - -This will enable the check and output to console and the writing of -the information to files with the given prefix (followed by testname), -ie :file:`/home/mydir/memcheck_test_bgp_multiview_topo1.txt` in case -of a memory leak. - Running Topotests with AddressSanitizer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -561,6 +535,48 @@ Here's an example of launching ``zebra`` and ``bgpd`` inside ``gdb`` on router --gdb-breakpoints=nb_config_diff \ all-protocol-startup +Reporting Memleaks with FRR Memory Statistics +""""""""""""""""""""""""""""""""""""""""""""" + +FRR reports all allocated FRR memory objects on exit to standard error. +Topotest can be run to report such output as errors in order to check for +memleaks in FRR memory allocations. Specifying the CLI argument +``--memleaks`` will enable reporting FRR-based memory allocations at exit as errors. + +.. code:: shell + + sudo -E pytest --memleaks all-protocol-startup + + +StdErr log from daemos after exit +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When running with ``--memleaks``, to enable the reporting of other, +non-memory related, messages seen on StdErr after the daemons exit, +the following env variable can be set:: + + export TOPOTESTS_CHECK_STDERR=Yes + +(The value doesn't matter at this time. The check is whether the env +variable exists or not.) There is no pass/fail on this reporting; the +Output will be reported to the console. + +Collect Memory Leak Information +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When running with ``--memleaks``, FRR processes report unfreed memory +allocations upon exit. To enable also reporting of memory leaks to a specific +location, define an environment variable ``TOPOTESTS_CHECK_MEMLEAK`` with the +file prefix, i.e.: + + export TOPOTESTS_CHECK_MEMLEAK="/home/mydir/memleak_" + +For tests that support the TOPOTESTS_CHECK_MEMLEAK environment variable, this +will enable output to the information to files with the given prefix (followed +by testname), e.g.,: +file:`/home/mydir/memcheck_test_bgp_multiview_topo1.txt` in case +of a memory leak. + Detecting Memleaks with Valgrind """""""""""""""""""""""""""""""" diff --git a/tests/topotests/conftest.py b/tests/topotests/conftest.py index 74e308dbc6..b78a2f1052 100755 --- a/tests/topotests/conftest.py +++ b/tests/topotests/conftest.py @@ -86,6 +86,12 @@ def pytest_addoption(parser): ), ) + parser.addoption( + "--memleaks", + action="store_true", + help="Report memstat results as errors", + ) + parser.addoption( "--pause", action="store_true", @@ -189,7 +195,7 @@ def pytest_addoption(parser): ) -def check_for_memleaks(): +def check_for_valgrind_memleaks(): assert topotest.g_pytest_config.option.valgrind_memleaks leaks = [] @@ -232,10 +238,46 @@ def check_for_memleaks(): pytest.fail("valgrind memleaks found for daemons: " + " ".join(daemons)) +def check_for_memleaks(): + leaks = [] + tgen = get_topogen() # pylint: disable=redefined-outer-name + latest = [] + existing = [] + if tgen is not None: + logdir = tgen.logdir + if hasattr(tgen, "memstat_existing_files"): + existing = tgen.memstat_existing_files + latest = glob.glob(os.path.join(logdir, "*/*.err")) + + daemons = [] + for vfile in latest: + if vfile in existing: + continue + with open(vfile, encoding="ascii") as vf: + vfcontent = vf.read() + num = vfcontent.count("memstats:") + if num: + existing.append(vfile) # have summary don't check again + emsg = "{} types in {}".format(num, vfile) + leaks.append(emsg) + daemon = re.match(r".*test[a-z_A-Z0-9\+]*/(.*)\.err", vfile).group(1) + daemons.append("{}({})".format(daemon, num)) + + if tgen is not None: + tgen.memstat_existing_files = existing + + if leaks: + logger.error("memleaks found:\n\t%s", "\n\t".join(leaks)) + pytest.fail("memleaks found for daemons: " + " ".join(daemons)) + + @pytest.fixture(autouse=True, scope="module") def module_check_memtest(request): yield if request.config.option.valgrind_memleaks: + if get_topogen() is not None: + check_for_valgrind_memleaks() + if request.config.option.memleaks: if get_topogen() is not None: check_for_memleaks() @@ -264,6 +306,8 @@ def pytest_runtest_call(item: pytest.Item) -> None: # Check for leaks if requested if item.config.option.valgrind_memleaks: + check_for_valgrind_memleaks() + if item.config.option.memleaks: check_for_memleaks() @@ -370,10 +414,22 @@ def pytest_configure(config): if config.option.topology_only and is_xdist: pytest.exit("Cannot use --topology-only with distributed test mode") + pytest.exit("Cannot use --topology-only with distributed test mode") + # Check environment now that we have config if not diagnose_env(rundir): pytest.exit("environment has errors, please read the logs in %s" % rundir) + # slave TOPOTESTS_CHECK_MEMLEAK to memleaks flag + if config.option.memleaks: + if "TOPOTESTS_CHECK_MEMLEAK" not in os.environ: + os.environ["TOPOTESTS_CHECK_MEMLEAK"] = "/dev/null" + else: + if "TOPOTESTS_CHECK_MEMLEAK" in os.environ: + del os.environ["TOPOTESTS_CHECK_MEMLEAK"] + if "TOPOTESTS_CHECK_STDERR" in os.environ: + del os.environ["TOPOTESTS_CHECK_STDERR"] + @pytest.fixture(autouse=True, scope="session") def setup_session_auto():