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.

(cherry picked from commit debf78dbed)
This commit is contained in:
Thomas Haller 2023-02-03 14:07:25 +01:00
parent 0e13811a4b
commit ba5cbedd2c
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)
###############################################################################