summary refs log tree commit diff
path: root/src/tests/client/test-client.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/tests/client/test-client.py')
-rwxr-xr-xsrc/tests/client/test-client.py342
1 files changed, 267 insertions, 75 deletions
diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py
index d025d12e..6b2c51f5 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,
@@ -108,6 +120,7 @@ import random
 import dbus.service
 import dbus.mainloop.glib
 import io
+import pexpect
 
 ###############################################################################
 
@@ -203,6 +216,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 +355,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 +404,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 +709,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 +838,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 +931,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 +1048,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 +1094,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
 
@@ -1130,7 +1208,14 @@ class TestNmcli(NmTestBase):
 
     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 +1314,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 +1327,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 +1359,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 +1488,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 +1534,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 +1744,126 @@ 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,
+        )
+
+    @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)
+
 
 ###############################################################################