test-client: add valgrind support for call_nmcli_pexpect() tests

This will allow to find some memory leaks and memory corruptions.

The bulk of the nmcli calls are still not hooked up with valgrind.
Since we call nmcli a thousand time, we could not just run valgrind with
all of them. We would have instead to enable it randomly. This is
more work.
This commit is contained in:
Thomas Haller 2023-02-03 14:07:25 +01:00
parent d1e6d53013
commit debf78dbed
No known key found for this signature in database
GPG key ID: 29C2366E4DFC5728
3 changed files with 146 additions and 38 deletions

View file

@ -5456,7 +5456,7 @@ endif
###############################################################################
check-local-tests-client: src/nmcli/nmcli src/tests/client/test-client.py
"$(srcdir)/src/tests/client/test-client.sh" "$(builddir)" "$(srcdir)" "$(PYTHON)" --
LIBTOOL="$(LIBTOOL)" "$(srcdir)/src/tests/client/test-client.sh" "$(builddir)" "$(srcdir)" "$(PYTHON)" --
check_local += check-local-tests-client

View file

@ -9,5 +9,8 @@ test(
python.path(),
'--',
],
env: [
'LIBTOOL=',
],
timeout: 120,
)

View file

@ -90,7 +90,12 @@ ENV_NM_TEST_ASAN_OPTIONS = "NM_TEST_ASAN_OPTIONS"
ENV_NM_TEST_LSAN_OPTIONS = "NM_TEST_LSAN_OPTIONS"
ENV_NM_TEST_UBSAN_OPTIONS = "NM_TEST_UBSAN_OPTIONS"
#
# Run nmcli under valgrind. If unset, we honor NMTST_USE_VALGRIND instead.
# Valgrind is always disabled, if NM_TEST_REGENERATE is enabled.
ENV_NM_TEST_VALGRIND = "NM_TEST_VALGRIND"
ENV_LIBTOOL = "LIBTOOL"
###############################################################################
import sys
@ -107,8 +112,10 @@ import fcntl
import dbus
import time
import random
import tempfile
import dbus.service
import dbus.mainloop.glib
import collections
import io
from signal import SIGINT
@ -474,6 +481,38 @@ class Util:
for color in [[], ["--color", "yes"]]:
yield mode + fmt + color
@staticmethod
def valgrind_check_log(valgrind_log, logname):
if valgrind_log is None:
return
fd, name = valgrind_log
os.close(fd)
if not os.path.isfile(name):
raise Exception("valgrind log %s unexpectedly does not exist" % (name,))
if os.path.getsize(name) != 0:
out = subprocess.run(
[
"sed",
"-e",
"/^--[0-9]\+-- WARNING: unhandled .* syscall: /,/^--[0-9]\+-- it at http.*\.$/d",
name,
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if out.returncode != 0:
raise Exception('Calling "sed" to search valgrind log failed')
if out.stdout:
print("valgrind log %s for %s is not empty:" % (name, logname))
print("\n%s\n" % (out.stdout.decode("utf-8", errors="replace"),))
raise Exception("valgrind log %s unexpectedly is not empty" % (name,))
os.remove(name)
###############################################################################
@ -520,6 +559,15 @@ class Configuration:
v = Util.is_bool(os.environ.get(ENV_NM_TEST_REGENERATE, None))
elif name == ENV_NM_TEST_WITH_LINENO:
v = Util.is_bool(os.environ.get(ENV_NM_TEST_WITH_LINENO, None))
elif name == ENV_NM_TEST_VALGRIND:
if self.get(ENV_NM_TEST_REGENERATE):
v = False
else:
v = os.environ.get(ENV_NM_TEST_VALGRIND, None)
if v:
v = Util.is_bool(v)
else:
v = Util.is_bool(os.environ.get("NMTST_USE_VALGRIND", None))
elif name in [
ENV_NM_TEST_ASAN_OPTIONS,
ENV_NM_TEST_LSAN_OPTIONS,
@ -536,6 +584,21 @@ class Configuration:
v = "print_stacktrace=1:halt_on_error=1"
else:
assert False
elif name == ENV_LIBTOOL:
v = os.environ.get(name, None)
if v is None:
v = os.path.abspath(
os.path.dirname(self.get(ENV_NM_TEST_CLIENT_NMCLI_PATH))
+ "/../../libtool"
)
if not os.path.isfile(v):
v = None
else:
v = [v]
elif not v:
v = None
else:
v = shlex.split(v)
else:
raise Exception()
self._values[name] = v
@ -796,6 +859,39 @@ class TestNmcli(unittest.TestCase):
return content_expect, results_expect
def nmcli_construct_argv(self, args, with_valgrind=None):
if with_valgrind is None:
with_valgrind = conf.get(ENV_NM_TEST_VALGRIND)
valgrind_log = None
cmd = conf.get(ENV_NM_TEST_CLIENT_NMCLI_PATH)
if with_valgrind:
valgrind_log = tempfile.mkstemp(prefix="nm-test-client-valgrind.")
argv = [
"valgrind",
"--quiet",
"--error-exitcode=37",
"--leak-check=full",
"--gen-suppressions=all",
(
"--suppressions="
+ PathConfiguration.top_srcdir()
+ "/valgrind.suppressions"
),
"--num-callers=100",
"--log-file=" + valgrind_log[1],
cmd,
]
libtool = conf.get(ENV_LIBTOOL)
if libtool:
argv = list(libtool) + ["--mode=execute"] + argv
else:
argv = [cmd]
argv.extend(args)
return argv, valgrind_log
def call_nmcli_l(
self,
args,
@ -879,10 +975,14 @@ class TestNmcli(unittest.TestCase):
)
def call_nmcli_pexpect(self, args):
env = self._env(extra_env={"NO_COLOR": "1"})
return pexpect.spawn(
conf.get(ENV_NM_TEST_CLIENT_NMCLI_PATH), args, timeout=10, env=env
)
argv, valgrind_log = self.nmcli_construct_argv(args)
pexp = pexpect.spawn(argv[0], argv[1:], timeout=10, env=env)
typ = collections.namedtuple("CallNmcliPexpect", ["pexp", "valgrind_log"])
return typ(pexp, valgrind_log)
def _env(
self, lang="C", calling_num=None, fatal_warnings=_DEFAULT_ARG, extra_env=None
@ -978,7 +1078,10 @@ class TestNmcli(unittest.TestCase):
else:
self.fail("invalid language %s" % (lang))
args = [conf.get(ENV_NM_TEST_CLIENT_NMCLI_PATH)] + list(args)
# Running under valgrind is not yet supported for those tests.
args, valgrind_log = self.nmcli_construct_argv(args, with_valgrind=False)
assert valgrind_log is None
if replace_stdout is not None:
replace_stdout = list(replace_stdout)
@ -1892,24 +1995,25 @@ class TestNmcli(unittest.TestCase):
@nm_test
def test_ask_mode(self):
nmc = self.call_nmcli_pexpect(["--ask", "c", "add"])
nmc.expect("Connection type:")
nmc.sendline("ethernet")
nmc.expect("Interface name:")
nmc.sendline("eth0")
nmc.expect("There are 3 optional settings for Wired Ethernet.")
nmc.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.sendline("no")
nmc.expect("There are 2 optional settings for IPv4 protocol.")
nmc.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.sendline("no")
nmc.expect("There are 2 optional settings for IPv6 protocol.")
nmc.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.sendline("no")
nmc.expect("There are 4 optional settings for Proxy.")
nmc.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.sendline("no")
nmc.expect("Connection 'ethernet' \(.*\) successfully added.")
nmc.expect(pexpect.EOF)
nmc.pexp.expect("Connection type:")
nmc.pexp.sendline("ethernet")
nmc.pexp.expect("Interface name:")
nmc.pexp.sendline("eth0")
nmc.pexp.expect("There are 3 optional settings for Wired Ethernet.")
nmc.pexp.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.pexp.sendline("no")
nmc.pexp.expect("There are 2 optional settings for IPv4 protocol.")
nmc.pexp.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.pexp.sendline("no")
nmc.pexp.expect("There are 2 optional settings for IPv6 protocol.")
nmc.pexp.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.pexp.sendline("no")
nmc.pexp.expect("There are 4 optional settings for Proxy.")
nmc.pexp.expect("Do you want to provide them\? \(yes/no\) \[yes]")
nmc.pexp.sendline("no")
nmc.pexp.expect("Connection 'ethernet' \(.*\) successfully added.")
nmc.pexp.expect(pexpect.EOF)
Util.valgrind_check_log(nmc.valgrind_log, "test_ask_mode")
@skip_without_pexpect
@nm_test
@ -1919,33 +2023,34 @@ class TestNmcli(unittest.TestCase):
# https://bugzilla.redhat.com/show_bug.cgi?id=2154288
raise unittest.SkipTest("test is known to randomly fail (rhbz#2154288)")
def start_mon():
def start_mon(self):
nmc = self.call_nmcli_pexpect(["monitor"])
nmc.expect("NetworkManager is running")
nmc.pexp.expect("NetworkManager is running")
return nmc
def end_mon(nmc):
nmc.kill(SIGINT)
nmc.expect(pexpect.EOF)
def end_mon(self, nmc):
nmc.pexp.kill(SIGINT)
nmc.pexp.expect(pexpect.EOF)
Util.valgrind_check_log(nmc.valgrind_log, "test_monitor")
nmc = start_mon()
nmc = start_mon(self)
self.srv.op_AddObj("WiredDevice", iface="eth0")
nmc.expect("eth0: device created\r\n")
nmc.pexp.expect("eth0: device created\r\n")
self.srv.addConnection(
{"connection": {"type": "802-3-ethernet", "id": "con-1"}}
)
nmc.expect("con-1: connection profile created\r\n")
nmc.pexp.expect("con-1: connection profile created\r\n")
end_mon(nmc)
end_mon(self, nmc)
nmc = start_mon()
nmc = start_mon(self)
self.srv_shutdown()
nmc.expect("eth0: device removed")
nmc.expect("con-1: connection profile removed")
nmc.expect("NetworkManager is stopped")
end_mon(nmc)
nmc.pexp.expect("eth0: device removed")
nmc.pexp.expect("con-1: connection profile removed")
nmc.pexp.expect("NetworkManager is stopped")
end_mon(self, nmc)
###############################################################################