diff options
| author | Michael Biebl <biebl@debian.org> | 2023-02-10 11:50:34 +0100 |
|---|---|---|
| committer | Michael Biebl <biebl@debian.org> | 2023-02-10 11:50:34 +0100 |
| commit | 1372848511cb896b80b51ed1a3e9606bd9816631 (patch) | |
| tree | 674792b9385bdef935988894b45f06b2af39f88c /src/tests/client/test-client.py | |
| parent | 40ec077ea305994c1fc2130add6787ca0c73e2c6 (diff) | |
New upstream version 1.42.0 upstream/1.42.0
Diffstat (limited to 'src/tests/client/test-client.py')
| -rwxr-xr-x | src/tests/client/test-client.py | 328 |
1 files changed, 262 insertions, 66 deletions
diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index 85a09ad2..e5febe96 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -90,26 +90,33 @@ 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" -import sys +ENV_LIBTOOL = "LIBTOOL" -import os +############################################################################### + +import collections +import dbus +import dbus.mainloop.glib +import dbus.service import errno -import unittest -import socket +import fcntl +import io import itertools -import subprocess -import shlex +import os +import random import re -import fcntl -import dbus +import shlex +import signal +import socket +import subprocess +import sys +import tempfile import time -import random -import dbus.service -import dbus.mainloop.glib -import io +import unittest import gi @@ -207,10 +214,10 @@ class Util: } @classmethod - def signal_no_to_str(cls, signal): - s = cls._signal_no_lookup.get(signal, None) + def signal_no_to_str(cls, sig): + s = cls._signal_no_lookup.get(sig, None) if s is None: - return "<unknown %d>" % (signal) + return "<unknown %d>" % (sig) return s @staticmethod @@ -228,6 +235,19 @@ class Util: return isinstance(s, t) @staticmethod + def is_bool(s, defval=False): + if s is None: + return defval + if isinstance(s, int): + return s != 0 + if isinstance(s, str): + if s.lower() in ["1", "y", "yes", "true", "on"]: + return True + if s.lower() in ["0", "n", "no", "false", "off"]: + return False + raise ValueError('Argument "%s" is not a boolean' % (s,)) + + @staticmethod def as_bytes(s): if Util.is_string(s): return s.encode("utf-8") @@ -250,7 +270,8 @@ class Util: ).search @staticmethod - def quote(s): + def shlex_quote(s): + # Reimplement shlex.quote(). if Util.python_has_version(3, 3): return shlex.quote(s) if not s: @@ -260,6 +281,11 @@ class Util: return "'" + s.replace("'", "'\"'\"'") + "'" @staticmethod + def shlex_join(args): + # Reimplement shlex.join() + return " ".join(Util.shlex_quote(s) for s in args) + + @staticmethod def popen_wait(p, timeout=0): (res, b_stdout, b_stderr) = Util.popen_wait_read( p, timeout=timeout, read_std_pipes=False @@ -454,6 +480,48 @@ 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) + + @staticmethod + def pexpect_expect_all(pexp, *pattern_list): + # This will call "pexpect.expect()" on pattern_list, + # expecting all entries to match exactly once, in any + # order. + pattern_list = list(pattern_list) + while pattern_list: + idx = pexp.expect(pattern_list) + del pattern_list[idx] + ############################################################################### @@ -492,14 +560,23 @@ class Configuration: # # Only by setting NM_TEST_CLIENT_CHECK_L10N=1, these tests are included # as well. - v = os.environ.get(ENV_NM_TEST_CLIENT_CHECK_L10N, "0") == "1" + v = Util.is_bool(os.environ.get(ENV_NM_TEST_CLIENT_CHECK_L10N, None)) elif name == ENV_NM_TEST_REGENERATE: # in the "regenerate" mode, the tests will rewrite the files on disk against # which we assert. That is useful, if there are intentional changes and # we want to regenerate the expected output. - v = os.environ.get(ENV_NM_TEST_REGENERATE, "0") == "1" + v = Util.is_bool(os.environ.get(ENV_NM_TEST_REGENERATE, None)) elif name == ENV_NM_TEST_WITH_LINENO: - v = os.environ.get(ENV_NM_TEST_WITH_LINENO, "0") == "1" + 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, @@ -516,6 +593,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 @@ -574,17 +666,37 @@ class NMStubServer: ) self._p = p - def shutdown(self): + def shutdown(self, kill_mode="random"): conn = self._conn p = self._p self._nmobj = None self._nmiface = None self._conn = None self._p = None - p.stdin.close() - p.kill() + + # The test stub service watches stdin and will do a proper + # shutdown when it closes. That means, to send signals about + # going away. + # On the other hand, just killing it will cause the process + # from dropping off the bus. + if kill_mode == "kill": + p.kill() + elif kill_mode == "stdin-close": + p.stdin.close() + else: + assert kill_mode == "random" + ops = [p.stdin.close, p.kill] + random.shuffle(ops) + ops[0]() + r = random.random() + if r < 0.75: + if r < 0.5: + time.sleep(r * 0.2) + ops[1]() + if Util.popen_wait(p, 1) is None: raise Exception("Stub service did not exit in time") + p.stdin.close() if self._conn_get_main_object(conn) is not None: raise Exception( "Stub service is not still here although it should shut down" @@ -720,20 +832,28 @@ class AsyncProcess: ############################################################################### -class NmTestBase(unittest.TestCase): +MAX_JOBS = 15 + + +class TestNmcli(unittest.TestCase): def __init__(self, *args, **kwargs): self._calling_num = {} self._skip_test_for_l10n_diff = [] self._async_jobs = [] self._results = [] self.srv = None - return unittest.TestCase.__init__(self, *args, **kwargs) - + unittest.TestCase.__init__(self, *args, **kwargs) -MAX_JOBS = 15 + def srv_start(self): + self.srv_shutdown() + self.srv = NMStubServer(self._testMethodName) + def srv_shutdown(self): + if self.srv is not None: + srv = self.srv + self.srv = None + srv.shutdown() -class TestNmcli(NmTestBase): def ReplaceTextConUuid(self, con_name, replacement): return Util.ReplaceTextSimple( Util.memoize_nullary(lambda: self.srv.findConnectionUuid(con_name)), @@ -768,6 +888,39 @@ class TestNmcli(NmTestBase): 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, @@ -851,10 +1004,14 @@ class TestNmcli(NmTestBase): ) def call_nmcli_pexpect(self, args): - env = self._env() - return pexpect.spawn( - conf.get(ENV_NM_TEST_CLIENT_NMCLI_PATH), args, timeout=5, env=env - ) + + env = self._env(extra_env={"NO_COLOR": "1"}) + 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 @@ -869,7 +1026,12 @@ class TestNmcli(NmTestBase): self.fail("invalid language %s" % (lang)) env = {} - for k in ["LD_LIBRARY_PATH", "DBUS_SESSION_BUS_ADDRESS"]: + for k in [ + "LD_LIBRARY_PATH", + "DBUS_SESSION_BUS_ADDRESS", + "LIBNM_CLIENT_DEBUG", + "LIBNM_CLIENT_DEBUG_FILE", + ]: val = os.environ.get(k, None) if val is not None: env[k] = val @@ -950,7 +1112,10 @@ class TestNmcli(NmTestBase): 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) @@ -1023,7 +1188,7 @@ class TestNmcli(NmTestBase): self.assertEqual(returncode, -5) if check_on_disk: - cmd = "$NMCLI %s" % (" ".join([Util.quote(a) for a in args[1:]])) + cmd = "$NMCLI %s" % (Util.shlex_join(args[1:]),) cmd = Util.replace_text(cmd, replace_cmd) if returncode < 0: @@ -1110,9 +1275,7 @@ class TestNmcli(NmTestBase): self.async_wait() - if self.srv is not None: - self.srv.shutdown() - self.srv = None + self.srv_shutdown() self._calling_num = None @@ -1228,7 +1391,7 @@ class TestNmcli(NmTestBase): def nm_test(func): def f(self): - self.srv = NMStubServer(self._testMethodName) + self.srv_start() func(self) self._nm_test_post() @@ -1866,24 +2029,60 @@ class TestNmcli(NmTestBase): @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 + def test_monitor(self): + def start_mon(self): + nmc = self.call_nmcli_pexpect(["monitor"]) + nmc.pexp.expect("NetworkManager is running") + return nmc + + def end_mon(self, nmc): + nmc.pexp.kill(signal.SIGINT) + nmc.pexp.expect(pexpect.EOF) + Util.valgrind_check_log(nmc.valgrind_log, "test_monitor") + + nmc = start_mon(self) + + self.srv.op_AddObj("WiredDevice", iface="eth0") + nmc.pexp.expect("eth0: device created\r\n") + + self.srv.addConnection( + {"connection": {"type": "802-3-ethernet", "id": "con-1"}} + ) + nmc.pexp.expect("con-1: connection profile created\r\n") + + end_mon(self, nmc) + + nmc = start_mon(self) + self.srv_shutdown() + Util.pexpect_expect_all( + nmc.pexp, + "con-1: connection profile removed", + "eth0: device removed", + ) + nmc.pexp.expect("NetworkManager is stopped") + end_mon(self, nmc) ############################################################################### @@ -1933,16 +2132,13 @@ def main(): "eval `dbus-launch --sh-syntax`;\n" + 'trap "kill $DBUS_SESSION_BUS_PID" EXIT;\n' + "\n" - + " ".join( + + Util.shlex_join( [ - Util.quote(a) - for a in [ - sys.executable, - __file__, - "--started-with-dbus-session", - ] - + sys.argv[1:] + sys.executable, + __file__, + "--started-with-dbus-session", ] + + sys.argv[1:] ) + " \n" + "", |