diff options
Diffstat (limited to 'src/tests/client/test-client.py')
| -rwxr-xr-x | src/tests/client/test-client.py | 383 |
1 files changed, 298 insertions, 85 deletions
diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py index d025d12e..85a09ad2 100755 --- a/src/tests/client/test-client.py +++ b/src/tests/client/test-client.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# coding: utf-8 from __future__ import print_function @@ -16,7 +17,7 @@ from __future__ import print_function # When adjusting the tests, or when making changes to nmcli that intentionally # change the output, the expected output must be regenerated. # -# For that, you'd setup your system correctly (see below) and then simply: +# For that, you'd setup your system correctly (see SETUP below) and then simply: # # $ NM_TEST_REGENERATE=1 make check-local-tests-client # # Or `NM_TEST_REGENERATE=1 make check -j 10` @@ -24,7 +25,17 @@ from __future__ import print_function # # The previous step regenerated the expected output. Review the changes # # and consider whether they are correct. Then commit the changes to git. # -# Setup: For regenerating the output, the translations must work. First: +# With meson, you can do +# $ meson -Ddocs=true --prefix=/tmp/nm1 build +# $ ninja -C build +# $ ninja -C build install +# $ NM_TEST_REGENERATE=1 ninja -C build test +# +# Beware that you need to install the sources, and beware to choose a prefix that doesn't +# mess up your system (see SETUP below). +# +# SETUP: For regenerating the output, the translations must work. First +# test whether the following works: # # 1) LANG=pl_PL.UTF-8 /usr/bin/nmcli --version # # Ensure that Polish output works for the system-installed nmcli. @@ -35,6 +46,7 @@ from __future__ import print_function # # On Debian, you might do: # # sed -i 's/^# \(pl_PL.UTF-8 .*\)$/\1/p' /etc/locale.gen # # locale-gen pl_PL.UTF-8 +# # On Fedora, you might install `glibc-langpack-pl` package. # # 2) LANG=pl_PL.UTF-8 ./src/nmcli/nmcli --version # # Ensure that the built nmcli has Polish locale working. If not, @@ -83,16 +95,6 @@ ENV_NM_TEST_UBSAN_OPTIONS = "NM_TEST_UBSAN_OPTIONS" import sys -try: - import gi - from gi.repository import GLib - - gi.require_version("NM", "1.0") - from gi.repository import NM -except Exception as e: - GLib = None - NM = None - import os import errno import unittest @@ -109,6 +111,29 @@ import dbus.service import dbus.mainloop.glib import io +import gi + +try: + from gi.repository import GLib +except ImportError: + GLib = None + +try: + gi.require_version("NM", "1.0") +except ValueError: + NM = None +else: + try: + from gi.repository import NM + except ImportError: + NM = None + +try: + import pexpect +except ImportError: + pexpect = None + + ############################################################################### @@ -203,6 +228,13 @@ class Util: return isinstance(s, t) @staticmethod + def as_bytes(s): + if Util.is_string(s): + return s.encode("utf-8") + assert isinstance(s, bytes) + return s + + @staticmethod def memoize_nullary(nullary_func): result = [] @@ -335,6 +367,48 @@ class Util: return None @staticmethod + def _replace_text_match_join(split_arr, replacement): + yield split_arr[0] + for t in split_arr[1:]: + yield (replacement,) + yield t + + @staticmethod + def ReplaceTextSimple(search, replacement): + # This gives a function that can be used by Util.replace_text(). + # The function replaces an input bytes string @t. It must either return + # a bytes string, a list containing bytes strings and/or 1-tuples (the + # latter containing one bytes string). + # The 1-tuple acts as a placeholder for atomic text, that cannot be replaced + # a second time. + # + # Search for replace_text_fcn in Util.replace_text() where this is called. + replacement = Util.as_bytes(replacement) + + if callable(search): + search_fcn = search + else: + search_fcn = lambda: search + + def replace_fcn(t): + assert isinstance(t, bytes) + search_txt = search_fcn() + if search_txt is None: + return t + search_txt = Util.as_bytes(search_txt) + return Util._replace_text_match_join(t.split(search_txt), replacement) + + return replace_fcn + + @staticmethod + def ReplaceTextRegex(pattern, replacement): + # See ReplaceTextSimple. + pattern = Util.as_bytes(pattern) + replacement = Util.as_bytes(replacement) + p = re.compile(pattern) + return lambda t: Util._replace_text_match_join(p.split(t), replacement) + + @staticmethod def replace_text(text, replace_arr): if not replace_arr: return text @@ -342,27 +416,17 @@ class Util: if needs_encode: text = text.encode("utf-8") text = [text] - for replace in replace_arr: - try: - v_search = replace[0]() - except TypeError: - v_search = replace[0] - assert v_search is None or Util.is_string(v_search) - if not v_search: - continue - v_replace = replace[1] - v_search = v_search.encode("utf-8") - v_replace = v_replace.encode("utf-8") + for replace_text_fcn in replace_arr: text2 = [] for t in text: - if isinstance(t, tuple): + # tuples are markers for atomic strings. They won't be replaced a second + # time. + if not isinstance(t, tuple): + t = replace_text_fcn(t) + if isinstance(t, bytes) or isinstance(t, tuple): text2.append(t) - continue - t2 = t.split(v_search) - text2.append(t2[0]) - for t3 in t2[1:]: - text2.append((v_replace,)) - text2.append(t3) + else: + text2.extend(t) text = text2 bb = b"".join([(t[0] if isinstance(t, tuple) else t) for t in text]) if needs_encode: @@ -657,13 +721,25 @@ class AsyncProcess: class NmTestBase(unittest.TestCase): - pass + 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) MAX_JOBS = 15 class TestNmcli(NmTestBase): + def ReplaceTextConUuid(self, con_name, replacement): + return Util.ReplaceTextSimple( + Util.memoize_nullary(lambda: self.srv.findConnectionUuid(con_name)), + replacement, + ) + @staticmethod def _read_expected(filename): results_expect = [] @@ -774,6 +850,47 @@ class TestNmcli(NmTestBase): frame, ) + 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 + ) + + def _env( + self, lang="C", calling_num=None, fatal_warnings=_DEFAULT_ARG, extra_env=None + ): + if lang == "C": + language = "" + elif lang == "de_DE.utf8": + language = "de" + elif lang == "pl_PL.UTF-8": + language = "pl" + else: + self.fail("invalid language %s" % (lang)) + + env = {} + for k in ["LD_LIBRARY_PATH", "DBUS_SESSION_BUS_ADDRESS"]: + val = os.environ.get(k, None) + if val is not None: + env[k] = val + env["LANG"] = lang + env["LANGUAGE"] = language + env["LIBNM_USE_SESSION_BUS"] = "1" + env["LIBNM_USE_NO_UDEV"] = "1" + env["TERM"] = "linux" + env["ASAN_OPTIONS"] = conf.get(ENV_NM_TEST_ASAN_OPTIONS) + env["LSAN_OPTIONS"] = conf.get(ENV_NM_TEST_LSAN_OPTIONS) + env["LBSAN_OPTIONS"] = conf.get(ENV_NM_TEST_UBSAN_OPTIONS) + env["XDG_CONFIG_HOME"] = PathConfiguration.srcdir() + if calling_num is not None: + env["NM_TEST_CALLING_NUM"] = str(calling_num) + if fatal_warnings is _DEFAULT_ARG or fatal_warnings: + env["G_DEBUG"] = "fatal-warnings" + if extra_env is not None: + for k, v in extra_env.items(): + env[k] = v + return env + def _call_nmcli( self, args, @@ -826,37 +943,13 @@ class TestNmcli(NmTestBase): if lang is None or lang == "C": lang = "C" - language = "" elif lang == "de": lang = "de_DE.utf8" - language = "de" elif lang == "pl": lang = "pl_PL.UTF-8" - language = "pl" else: self.fail("invalid language %s" % (lang)) - env = {} - if extra_env is not None: - for k, v in extra_env.items(): - env[k] = v - for k in ["LD_LIBRARY_PATH", "DBUS_SESSION_BUS_ADDRESS"]: - val = os.environ.get(k, None) - if val is not None: - env[k] = val - env["LANG"] = lang - env["LANGUAGE"] = language - env["LIBNM_USE_SESSION_BUS"] = "1" - env["LIBNM_USE_NO_UDEV"] = "1" - env["TERM"] = "linux" - env["ASAN_OPTIONS"] = conf.get(ENV_NM_TEST_ASAN_OPTIONS) - env["LSAN_OPTIONS"] = conf.get(ENV_NM_TEST_LSAN_OPTIONS) - env["LBSAN_OPTIONS"] = conf.get(ENV_NM_TEST_UBSAN_OPTIONS) - env["XDG_CONFIG_HOME"] = PathConfiguration.srcdir() - env["NM_TEST_CALLING_NUM"] = str(calling_num) - if fatal_warnings is _DEFAULT_ARG or fatal_warnings: - env["G_DEBUG"] = "fatal-warnings" - args = [conf.get(ENV_NM_TEST_CLIENT_NMCLI_PATH)] + list(args) if replace_stdout is not None: @@ -967,6 +1060,7 @@ class TestNmcli(NmTestBase): "content": content, } + env = self._env(lang, calling_num, fatal_warnings, extra_env) async_job = AsyncProcess(args=args, env=env, complete_cb=complete_cb) self._async_jobs.append(async_job) @@ -1012,26 +1106,22 @@ class TestNmcli(NmTestBase): def async_wait(self): return self.async_start(wait_all=True) - def _nm_test_pre(self): - self._calling_num = {} - self._skip_test_for_l10n_diff = [] - self._async_jobs = [] - self._results = [] - - self.srv = NMStubServer(self._testMethodName) - def _nm_test_post(self): self.async_wait() - self.srv.shutdown() - self.srv = None + if self.srv is not None: + self.srv.shutdown() + self.srv = None self._calling_num = None results = self._results self._results = None + if len(results) == 0: + return + skip_test_for_l10n_diff = self._skip_test_for_l10n_diff self._skip_test_for_l10n_diff = None @@ -1128,9 +1218,24 @@ class TestNmcli(NmTestBase): % (",".join(skip_test_for_l10n_diff)) ) + def skip_without_pexpect(func): + def f(self): + if pexpect is None: + raise unittest.SkipTest("pexpect not available") + func(self) + + return f + def nm_test(func): def f(self): - self._nm_test_pre() + self.srv = NMStubServer(self._testMethodName) + func(self) + self._nm_test_post() + + return f + + def nm_test_no_dbus(func): + def f(self): func(self) self._nm_test_post() @@ -1229,10 +1334,7 @@ class TestNmcli(NmTestBase): replace_uuids = [] replace_uuids.append( - ( - Util.memoize_nullary(lambda: self.srv.findConnectionUuid("con-xx1")), - "UUID-con-xx1-REPLACED-REPLACED-REPLA", - ) + self.ReplaceTextConUuid("con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA") ) self.call_nmcli( @@ -1245,9 +1347,8 @@ class TestNmcli(NmTestBase): for con_name, apn in con_gsm_list: replace_uuids.append( - ( - Util.memoize_nullary(lambda: self.srv.findConnectionUuid(con_name)), - "UUID-" + con_name + "-REPLACED-REPLACED-REPL", + self.ReplaceTextConUuid( + con_name, "UUID-" + con_name + "-REPLACED-REPLACED-REPL" ) ) @@ -1278,10 +1379,7 @@ class TestNmcli(NmTestBase): ) replace_uuids.append( - ( - Util.memoize_nullary(lambda: self.srv.findConnectionUuid("ethernet")), - "UUID-ethernet-REPLACED-REPLACED-REPL", - ) + self.ReplaceTextConUuid("ethernet", "UUID-ethernet-REPLACED-REPLACED-REPL") ) self.call_nmcli( @@ -1410,10 +1508,7 @@ class TestNmcli(NmTestBase): replace_uuids = [] replace_uuids.append( - ( - Util.memoize_nullary(lambda: self.srv.findConnectionUuid("con-xx1")), - "UUID-con-xx1-REPLACED-REPLACED-REPLA", - ) + self.ReplaceTextConUuid("con-xx1", "UUID-con-xx1-REPLACED-REPLACED-REPLA") ) self.call_nmcli( @@ -1459,10 +1554,7 @@ class TestNmcli(NmTestBase): self.async_wait() replace_uuids.append( - ( - Util.memoize_nullary(lambda: self.srv.findConnectionUuid("con-vpn-1")), - "UUID-con-vpn-1-REPLACED-REPLACED-REP", - ) + self.ReplaceTextConUuid("con-vpn-1", "UUID-con-vpn-1-REPLACED-REPLACED-REP") ) self.call_nmcli( @@ -1672,6 +1764,127 @@ class TestNmcli(NmTestBase): replace_cmd=replace_uuids, ) + @nm_test_no_dbus + def test_offline(self): + + # Make sure we're not using D-Bus + no_dbus_env = { + "DBUS_SYSTEM_BUS_ADDRESS": "very:invalid", + "DBUS_SESSION_BUS_ADDRESS": "very:invalid", + } + + # This check just makes sure the above works and the + # "nmcli g" command indeed fails talking to D-Bus + self.call_nmcli( + ["g"], + extra_env=no_dbus_env, + replace_stderr=[ + Util.ReplaceTextRegex( + # depending on glib version, it prints `%s', '%s', or ā%sā. + # depending on libc version, it converts unicode to ? or *. + r"Key/Value pair 0, [`*?']invalid[*?'], in address element [`*?']very:invalid[*?'] does not contain an equal sign", + "Key/Value pair 0, 'invalid', in address element 'very:invalid' does not contain an equal sign", + ) + ], + ) + + replace_uuids = [ + Util.ReplaceTextRegex( + r"\buuid=[-a-f0-9]+\b", "uuid=UUID-WAS-HERE-BUT-IS-NO-MORE-SADLY" + ) + ] + + self.call_nmcli( + ["--offline", "c", "add", "type", "ethernet"], + extra_env=no_dbus_env, + replace_stdout=replace_uuids, + ) + + self.call_nmcli( + ["--offline", "c", "show"], + extra_env=no_dbus_env, + ) + + self.call_nmcli( + ["--offline", "g"], + extra_env=no_dbus_env, + ) + + self.call_nmcli( + ["--offline"], + extra_env=no_dbus_env, + ) + + self.call_nmcli( + [ + "--offline", + "c", + "add", + "type", + "wifi", + "ssid", + "lala", + "802-1x.eap", + "pwd", + "802-1x.identity", + "foo", + "802-1x.password", + "bar", + ], + extra_env=no_dbus_env, + replace_stdout=replace_uuids, + ) + + self.call_nmcli( + [ + "--offline", + "c", + "add", + "type", + "wifi", + "ssid", + "lala", + "802-1x.eap", + "pwd", + "802-1x.identity", + "foo", + "802-1x.password", + "bar", + "802-1x.password-flags", + "agent-owned", + ], + extra_env=no_dbus_env, + replace_stdout=replace_uuids, + ) + + self.call_nmcli( + ["--complete-args", "--offline", "conn", "modify", "ipv6.ad"], + extra_env=no_dbus_env, + ) + + @skip_without_pexpect + @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) + ############################################################################### |