summary refs log tree commit diff
path: root/src/tests/client/test-client.py
diff options
context:
space:
mode:
authorMichael Biebl <biebl@debian.org>2025-02-12 13:46:50 +0100
committerMichael Biebl <biebl@debian.org>2025-02-12 13:46:50 +0100
commit8bdf070ff046f482f6eb5e2b15ebc216f5d1e3da (patch)
treef706478d189d54c6532e8863d4b0d5ff5575af60 /src/tests/client/test-client.py
parent818258cf34b83fbc754633295e1052d4752d7b15 (diff)
New upstream version 1.51.90 upstream/1.51.90
Diffstat (limited to 'src/tests/client/test-client.py')
-rwxr-xr-xsrc/tests/client/test-client.py727
1 files changed, 459 insertions, 268 deletions
diff --git a/src/tests/client/test-client.py b/src/tests/client/test-client.py
index 60853e6d..d01b3bce 100755
--- a/src/tests/client/test-client.py
+++ b/src/tests/client/test-client.py
@@ -19,18 +19,14 @@ from __future__ import print_function
 #
 # 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`
+#  $ meson -Ddocs=true --prefix=/tmp/nm1 build
+#  $ ninja -C build
+#  $ ninja -C build install
+#  $ NM_TEST_REGENERATE=1 ninja -C build test
 #  $ git diff ... ; git add ...
 #    # The previous step regenerated the expected output. Review the changes
 #    # and consider whether they are correct. Then commit the changes to git.
 #
-#   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).
 #
@@ -52,7 +48,7 @@ from __future__ import print_function
 #    # Ensure that the built nmcli has Polish locale working. If not,
 #    # you probably need to first `make install` the application at the
 #    # correct prefix. Take care to configure the build with the desired
-#    # prefix, like `./configure --prefix=/opt/tmp`. Usually, you want to avoid
+#    # prefix, like `meson setup build --prefix=/opt/tmp`. Usually, you want to avoid
 #    # using /usr as prefix, because that might overwrite files from your
 #    # package management system.
 #
@@ -90,6 +86,9 @@ ENV_NM_TEST_REGENERATE = "NM_TEST_REGENERATE"
 # numbers enabled.
 ENV_NM_TEST_WITH_LINENO = "NM_TEST_WITH_LINENO"
 
+# Log pexpect output to stderr, for debuging
+ENV_NM_TEST_LOG_PEXPECT = "NM_TEST_LOG_PEXPECT"
+
 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"
@@ -689,29 +688,6 @@ class Util:
         return argv, valgrind_log
 
     @staticmethod
-    def cmd_call_pexpect(cmd_path, args, extra_env):
-        argv, valgrind_log = Util.cmd_create_argv(cmd_path, args)
-        env = Util.cmd_create_env(extra_env=extra_env)
-
-        pexp = pexpect.spawn(argv[0], argv[1:], timeout=10, env=env)
-
-        pexp.str_last_chars = 100000
-
-        typ = collections.namedtuple("CallPexpect", ["pexp", "valgrind_log"])
-        return typ(pexp, valgrind_log)
-
-    @staticmethod
-    def cmd_call_pexpect_nmcli(args, extra_env={}):
-        extra_env = extra_env.copy()
-        extra_env.update({"NO_COLOR": "1"})
-
-        return Util.cmd_call_pexpect(
-            ENV_NM_TEST_CLIENT_NMCLI_PATH,
-            args,
-            extra_env,
-        )
-
-    @staticmethod
     def get_nmcli_version():
         ver = NM.utils_version()
         micro = ver & 0xFF
@@ -737,7 +713,7 @@ class Configuration:
             )
             if not os.path.isdir(v):
                 raise Exception("Missing builddir. Set NM_TEST_CLIENT_BUILDDIR?")
-        elif name == ENV_NM_TEST_CLIENT_NMCLI_PATH:
+        elif name == "ENV_NM_TEST_CLIENT_NMCLI_UNCHECKED_PATH":
             v = os.environ.get(ENV_NM_TEST_CLIENT_NMCLI_PATH, None)
             if v is None:
                 try:
@@ -746,6 +722,8 @@ class Configuration:
                     )
                 except:
                     pass
+        elif name == ENV_NM_TEST_CLIENT_NMCLI_PATH:
+            v = self.get("ENV_NM_TEST_CLIENT_NMCLI_UNCHECKED_PATH")
             if not os.path.exists(v):
                 raise Exception("Missing nmcli binary. Set NM_TEST_CLIENT_NMCLI_PATH?")
         elif name == ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH:
@@ -779,6 +757,8 @@ 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_LOG_PEXPECT:
+            v = Util.is_bool(os.environ.get(ENV_NM_TEST_LOG_PEXPECT, None))
         elif name == ENV_NM_TEST_VALGRIND:
             if self.get(ENV_NM_TEST_REGENERATE):
                 v = False
@@ -808,7 +788,7 @@ class Configuration:
             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))
+                    os.path.dirname(self.get("ENV_NM_TEST_CLIENT_NMCLI_UNCHECKED_PATH"))
                     + "/../../libtool"
                 )
                 if not os.path.isfile(v):
@@ -1071,6 +1051,7 @@ class NMTestContext:
         self._calling_num = {}
         self._skip_test_for_l10n_diff = []
         self._async_jobs = []
+        self._nmc = None
         self.ctx_results = []
         self.srv = None
 
@@ -1129,112 +1110,60 @@ class NMTestContext:
     def async_append_job(self, async_job):
         self._async_jobs.append(async_job)
 
-    def run_post(self):
-        self.async_wait()
+    def cmd_call_pexpect(self, cmd_path, args, extra_env):
+        if self._nmc is not None:
+            raise Exception("Unfinished pexpect run exists")
 
-        self.srv_shutdown()
+        argv, valgrind_log = Util.cmd_create_argv(cmd_path, args)
+        env = Util.cmd_create_env(extra_env=extra_env)
 
-        self._calling_num = None
+        pexp = pexpect.spawn(argv[0], argv[1:], timeout=10, env=env, encoding="utf-8")
+        if conf.get(ENV_NM_TEST_LOG_PEXPECT):
+            pexp.logfile = sys.stderr
 
-        results = self.ctx_results
-        self.ctx_results = None
+        pexp.str_last_chars = 100000
 
-        if len(results) == 0:
-            return
+        typ = collections.namedtuple("CallPexpect", ["pexp", "valgrind_log"])
+        self._nmc = typ(pexp, valgrind_log)
+        return pexp
 
-        skip_test_for_l10n_diff = self._skip_test_for_l10n_diff
-        self._skip_test_for_l10n_diff = None
+    def cmd_call_pexpect_nmcli(self, args, extra_env={}):
+        extra_env = extra_env.copy()
+        extra_env.update({"NO_COLOR": "1"})
 
-        filename = os.path.abspath(
-            PathConfiguration.srcdir()
-            + "/test-client.check-on-disk/"
-            + self.testMethodName
-            + ".expected"
+        return self.cmd_call_pexpect(
+            ENV_NM_TEST_CLIENT_NMCLI_PATH,
+            args,
+            extra_env,
         )
 
-        regenerate = conf.get(ENV_NM_TEST_REGENERATE)
+    def cmd_close_pexpect(self, pexp=None, signal=None):
+        if self._nmc is None:
+            raise Exception("No pexpect run exists")
 
-        content_expect, results_expect = Util.file_read_expected(filename)
+        if signal is not None:
+            pexp.kill(signal)
+        pexp.expect(pexpect.EOF)
+        pexp.close()
 
-        if results_expect is None:
-            if not regenerate:
-                self.fail(
-                    "Failed to parse expected file '%s'. Let the test write the file by rerunning with NM_TEST_REGENERATE=1"
-                    % (filename)
-                )
-        else:
-            for i in range(0, min(len(results_expect), len(results))):
-                n = results[i]
-                if results_expect[i] == n["content"]:
-                    continue
-                if regenerate:
-                    continue
-                if n["ignore_l10n_diff"]:
-                    skip_test_for_l10n_diff.append(n["test_name"])
-                    continue
-                print(
-                    "\n\n\nThe file '%s' does not have the expected content:"
-                    % (filename)
-                )
-                print("ACTUAL OUTPUT:\n[[%s]]\n" % (n["content"]))
-                print("EXPECT OUTPUT:\n[[%s]]\n" % (results_expect[i]))
-                print(
-                    "Let the test write the file by rerunning with NM_TEST_REGENERATE=1"
-                )
-                print(
-                    "See howto in %s for details.\n"
-                    % (PathConfiguration.canonical_script_filename())
-                )
-                sys.stdout.flush()
-                self.fail(
-                    "Unexpected output of command, expected %s. Rerun test with NM_TEST_REGENERATE=1 to regenerate files"
-                    % (filename)
-                )
-            if len(results_expect) != len(results):
-                if not regenerate:
-                    print(
-                        "\n\n\nThe number of tests in %s does not match the expected content (%s vs %s):"
-                        % (filename, len(results_expect), len(results))
-                    )
-                    if len(results_expect) < len(results):
-                        print(
-                            "ACTUAL OUTPUT:\n[[%s]]\n"
-                            % (results[len(results_expect)]["content"])
-                        )
-                    else:
-                        print(
-                            "EXPECT OUTPUT:\n[[%s]]\n" % (results_expect[len(results)])
-                        )
-                    print(
-                        "Let the test write the file by rerunning with NM_TEST_REGENERATE=1"
-                    )
-                    print(
-                        "See howto in %s for details.\n"
-                        % (PathConfiguration.canonical_script_filename())
-                    )
-                    sys.stdout.flush()
-                    self.fail(
-                        "Unexpected output of command, expected %s. Rerun test with NM_TEST_REGENERATE=1 to regenerate files"
-                        % (filename)
-                    )
+        valgrind_log = self._nmc.valgrind_log
+        self._nmc = None
+        return (pexp.exitstatus, pexp.signalstatus, valgrind_log)
 
-        if regenerate:
-            content_new = b"".join([r["content"] for r in results])
-            if content_new != content_expect:
-                try:
-                    with open(filename, "wb") as content_file:
-                        content_file.write(content_new)
-                except Exception as e:
-                    self.fail("Failure to write '%s': %s" % (filename, e))
+    def pexpect_cleanup(self):
+        if self._nmc is None:
+            return
 
-        if skip_test_for_l10n_diff:
-            # nmcli loads translations from the installation path. This failure commonly
-            # happens because you did not install the binary in the --prefix, before
-            # running the test. Hence, translations are not available or differ.
-            raise unittest.SkipTest(
-                "Skipped asserting for localized tests %s. Set NM_TEST_CLIENT_CHECK_L10N=1 to force fail."
-                % (",".join(skip_test_for_l10n_diff))
+        (exitstatus, signalstatus, _valgrind_log) = self.cmd_close_pexpect(
+            self._nmc.pexp
+        )
+        if signalstatus is not None:
+            print(
+                "*** pexpect'd process killed by %s ***"
+                % Util.signal_no_to_str(signalstatus)
             )
+        if exitstatus is not None:
+            print("*** pexpect'd process exited with status = %d ***" % exitstatus)
 
 
 ###############################################################################
@@ -1245,6 +1174,7 @@ class TestNmcli(unittest.TestCase):
         Util.skip_without_dbus_session()
         Util.skip_without_NM()
         self.ctx = NMTestContext(self._testMethodName)
+        self._skip_test_for_l10n_diff = []
 
     def call_nmcli_l(
         self,
@@ -1505,18 +1435,125 @@ class TestNmcli(unittest.TestCase):
 
         self.ctx.async_start(wait_all=sync_barrier)
 
+    def run_post(self):
+        self.ctx.async_wait()
+        self.ctx.srv_shutdown()
+        self.ctx.pexpect_cleanup()
+
+        self.ctx._calling_num = None
+
+        results = self.ctx.ctx_results
+        self.ctx.ctx_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
+
+        filename = os.path.abspath(
+            PathConfiguration.srcdir()
+            + "/test-client.check-on-disk/"
+            + self._testMethodName
+            + ".expected"
+        )
+
+        regenerate = conf.get(ENV_NM_TEST_REGENERATE)
+
+        content_expect, results_expect = Util.file_read_expected(filename)
+
+        if results_expect is None:
+            if not regenerate:
+                self.fail(
+                    "Failed to parse expected file '%s'. Let the test write the file by rerunning with NM_TEST_REGENERATE=1"
+                    % (filename)
+                )
+        else:
+            for i in range(0, min(len(results_expect), len(results))):
+                n = results[i]
+                if results_expect[i] == n["content"]:
+                    continue
+                if regenerate:
+                    continue
+                if n["ignore_l10n_diff"]:
+                    skip_test_for_l10n_diff.append(n["test_name"])
+                    continue
+                print(
+                    "\n\n\nThe file '%s' does not have the expected content:"
+                    % (filename)
+                )
+                print("ACTUAL OUTPUT:\n[[%s]]\n" % (n["content"]))
+                print("EXPECT OUTPUT:\n[[%s]]\n" % (results_expect[i]))
+                print(
+                    "Let the test write the file by rerunning with NM_TEST_REGENERATE=1"
+                )
+                print(
+                    "See howto in %s for details.\n"
+                    % (PathConfiguration.canonical_script_filename())
+                )
+                sys.stdout.flush()
+                self.fail(
+                    "Unexpected output of command, expected %s. Rerun test with NM_TEST_REGENERATE=1 to regenerate files"
+                    % (filename)
+                )
+            if len(results_expect) != len(results):
+                if not regenerate:
+                    print(
+                        "\n\n\nThe number of tests in %s does not match the expected content (%s vs %s):"
+                        % (filename, len(results_expect), len(results))
+                    )
+                    if len(results_expect) < len(results):
+                        print(
+                            "ACTUAL OUTPUT:\n[[%s]]\n"
+                            % (results[len(results_expect)]["content"])
+                        )
+                    else:
+                        print(
+                            "EXPECT OUTPUT:\n[[%s]]\n" % (results_expect[len(results)])
+                        )
+                    print(
+                        "Let the test write the file by rerunning with NM_TEST_REGENERATE=1"
+                    )
+                    print(
+                        "See howto in %s for details.\n"
+                        % (PathConfiguration.canonical_script_filename())
+                    )
+                    sys.stdout.flush()
+                    self.fail(
+                        "Unexpected output of command, expected %s. Rerun test with NM_TEST_REGENERATE=1 to regenerate files"
+                        % (filename)
+                    )
+
+        if regenerate:
+            content_new = b"".join([r["content"] for r in results])
+            if content_new != content_expect:
+                try:
+                    with open(filename, "wb") as content_file:
+                        content_file.write(content_new)
+                except Exception as e:
+                    self.fail("Failure to write '%s': %s" % (filename, e))
+
+        if skip_test_for_l10n_diff:
+            # nmcli loads translations from the installation path. This failure commonly
+            # happens because you did not install the binary in the --prefix, before
+            # running the test. Hence, translations are not available or differ.
+            raise unittest.SkipTest(
+                "Skipped asserting for localized tests %s. Set NM_TEST_CLIENT_CHECK_L10N=1 to force fail."
+                % (",".join(skip_test_for_l10n_diff))
+            )
+
     def nm_test(func):
         def f(self):
             self.ctx.srv_start()
             func(self)
-            self.ctx.run_post()
+            self.run_post()
 
         return f
 
     def nm_test_no_dbus(func):
         def f(self):
             func(self)
-            self.ctx.run_post()
+            self.run_post()
 
         return f
 
@@ -2188,26 +2225,31 @@ class TestNmcli(unittest.TestCase):
     @Util.skip_without_pexpect
     @nm_test
     def test_ask_mode(self):
-        nmc = Util.cmd_call_pexpect_nmcli(["--ask", "c", "add"])
-        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(r"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(r"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(r"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(r"Do you want to provide them\? \(yes/no\) \[yes]")
-        nmc.pexp.sendline("no")
-        nmc.pexp.expect(r"Connection 'ethernet' \(.*\) successfully added.")
-        nmc.pexp.expect(pexpect.EOF)
-        Util.valgrind_check_log(nmc.valgrind_log, "test_ask_mode")
+        pexp = self.ctx.cmd_call_pexpect_nmcli(["--ask", "c", "add"])
+        pexp.expect("Connection type:")
+        pexp.sendline("ethernet")
+        pexp.expect("Interface name:")
+        pexp.sendline("eth0")
+        pexp.expect("There are 3 optional settings for Wired Ethernet.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect("There are 2 optional settings for IPv4 protocol.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect("There are 2 optional settings for IPv6 protocol.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect("There are 4 optional settings for Proxy.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect(r"Connection 'ethernet' \(.*\) successfully added.")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_ask_mode")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
     @Util.skip_without_pexpect
     @nm_test
@@ -2218,26 +2260,26 @@ class TestNmcli(unittest.TestCase):
             "DBUS_SESSION_BUS_ADDRESS": "very:invalid",
         }
 
-        nmc = Util.cmd_call_pexpect_nmcli(
+        pexp = self.ctx.cmd_call_pexpect_nmcli(
             ["--offline", "--ask", "c", "add"], extra_env=no_dbus_env
         )
-        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(r"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(r"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(r"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(r"Do you want to provide them\? \(yes/no\) \[yes]")
-        nmc.pexp.sendline("no")
-        nmc.pexp.expect(
+        pexp.expect("Connection type:")
+        pexp.sendline("ethernet")
+        pexp.expect("Interface name:")
+        pexp.sendline("eth0")
+        pexp.expect("There are 3 optional settings for Wired Ethernet.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect("There are 2 optional settings for IPv4 protocol.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect("There are 2 optional settings for IPv6 protocol.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect("There are 4 optional settings for Proxy.")
+        pexp.expect(r"Do you want to provide them\? \(yes/no\) \[yes]")
+        pexp.sendline("no")
+        pexp.expect(
             r"\[connection\]\r\n"
             r"id=ethernet\r\n"
             r"uuid=.*\r\n"
@@ -2255,43 +2297,58 @@ class TestNmcli(unittest.TestCase):
             r"\r\n"
             r"\[proxy\]\r\n"
         )
-        nmc.pexp.expect(pexpect.EOF)
-        Util.valgrind_check_log(nmc.valgrind_log, "test_ask_offline")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_ask_offline")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
     @Util.skip_without_pexpect
     @nm_test
     def test_monitor(self):
         def start_mon(self):
-            nmc = Util.cmd_call_pexpect_nmcli(["monitor"])
-            nmc.pexp.expect("NetworkManager is running")
-            return nmc
+            pexp = self.ctx.cmd_call_pexpect_nmcli(["monitor"])
+            pexp.expect("NetworkManager is running")
+            return pexp
 
-        def end_mon(self, nmc):
-            nmc.pexp.kill(signal.SIGINT)
-            nmc.pexp.expect(pexpect.EOF)
-            Util.valgrind_check_log(nmc.valgrind_log, "test_monitor")
+        def end_mon(self, pexp):
+            (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(
+                pexp, signal=signal.SIGINT
+            )
+            Util.valgrind_check_log(valgrind_log, "test_monitor")
+            self.assertIsNone(
+                signalstatus,
+                "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+            )
+            self.assertEqual(
+                exitstatus,
+                128 + signal.SIGINT,
+                "Unexpectedly returned a non-zero status",
+            )
 
-        nmc = start_mon(self)
+        pexp = start_mon(self)
 
         self.ctx.srv.op_AddObj("WiredDevice", iface="eth0")
-        nmc.pexp.expect("eth0: device created\r\n")
+        pexp.expect("eth0: device created\r\n")
 
         self.ctx.srv.addConnection(
             {"connection": {"type": "802-3-ethernet", "id": "con-1"}}
         )
-        nmc.pexp.expect("con-1: connection profile created\r\n")
+        pexp.expect("con-1: connection profile created\r\n")
 
-        end_mon(self, nmc)
+        end_mon(self, pexp)
 
-        nmc = start_mon(self)
+        pexp = start_mon(self)
         self.ctx.srv_shutdown()
         Util.pexpect_expect_all(
-            nmc.pexp,
+            pexp,
             "con-1: connection profile removed",
             "eth0: device removed",
         )
-        nmc.pexp.expect("NetworkManager is stopped")
-        end_mon(self, nmc)
+        pexp.expect("NetworkManager is stopped")
+        end_mon(self, pexp)
 
     @nm_test_no_dbus  # we need dbus, but we need to pass arguments to srv_start
     def test_version_warn(self):
@@ -2373,7 +2430,9 @@ class TestNmCloudSetup(unittest.TestCase):
                 func(self)
             except Exception as e:
                 error = e
-            self.ctx.run_post()
+            self.ctx.async_wait()
+            self.ctx.srv_shutdown()
+            self.ctx.pexpect_cleanup()
 
             self.md_conn.close()
             p.stdin.close()
@@ -2456,7 +2515,8 @@ class TestNmCloudSetup(unittest.TestCase):
         )
 
         # Run nm-cloud-setup for the first time
-        nmc = Util.cmd_call_pexpect(
+
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2466,19 +2526,25 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider aliyun detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("get-config: start fetching meta data")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider aliyun detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: start fetching meta data")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # One of the devices has no IPv4 configuration to be modified
-        nmc.pexp.expect("device has no suitable applied connection. Skip")
+        pexp.expect("device has no suitable applied connection. Skip")
         # The other one was lacking an address set it up.
-        nmc.pexp.expect("some changes were applied for provider aliyun")
-        nmc.pexp.expect(pexpect.EOF)
+        pexp.expect("some changes were applied for provider aliyun")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_aliyun")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
         # Run nm-cloud-setup for the second time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2488,17 +2554,21 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider aliyun detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("get-config: starting")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider aliyun detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # No changes this time
-        nmc.pexp.expect('device needs no update to applied connection "con-eth0"')
-        nmc.pexp.expect("no changes were applied for provider aliyun")
-        nmc.pexp.expect(pexpect.EOF)
-
-        Util.valgrind_check_log(nmc.valgrind_log, "test_aliyun")
+        pexp.expect('device needs no update to applied connection "con-eth0"')
+        pexp.expect("no changes were applied for provider aliyun")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_aliyun")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
     @cloud_setup_test
     def test_azure(self):
@@ -2535,7 +2605,7 @@ class TestNmCloudSetup(unittest.TestCase):
         self._mock_path(_azure_iface + "1/ipv4/subnet/0/prefix/" + _azure_query, "20")
 
         # Run nm-cloud-setup for the first time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2545,26 +2615,32 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider azure detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("found azure interfaces: 2")
-        nmc.pexp.expect(r"interface\[0]: found a matching device with hwaddr")
-        nmc.pexp.expect(
+        pexp.expect("provider azure detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("found azure interfaces: 2")
+        pexp.expect(r"interface\[0]: found a matching device with hwaddr")
+        pexp.expect(
             r"interface\[0]: (received subnet address|received subnet prefix 20)"
         )
-        nmc.pexp.expect(
+        pexp.expect(
             r"interface\[0]: (received subnet address|received subnet prefix 20)"
         )
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # One of the devices has no IPv4 configuration to be modified
-        nmc.pexp.expect("device has no suitable applied connection. Skip")
+        pexp.expect("device has no suitable applied connection. Skip")
         # The other one was lacking an address set it up.
-        nmc.pexp.expect("some changes were applied for provider azure")
-        nmc.pexp.expect(pexpect.EOF)
+        pexp.expect("some changes were applied for provider azure")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_azure")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
         # Run nm-cloud-setup for the second time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2574,17 +2650,21 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider azure detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("get-config: starting")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider azure detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # No changes this time
-        nmc.pexp.expect('device needs no update to applied connection "con-eth0"')
-        nmc.pexp.expect("no changes were applied for provider azure")
-        nmc.pexp.expect(pexpect.EOF)
-
-        Util.valgrind_check_log(nmc.valgrind_log, "test_azure")
+        pexp.expect('device needs no update to applied connection "con-eth0"')
+        pexp.expect("no changes were applied for provider azure")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_azure")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
     @cloud_setup_test
     def test_ec2(self):
@@ -2611,7 +2691,7 @@ class TestNmCloudSetup(unittest.TestCase):
         )
 
         # Run nm-cloud-setup for the first time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2621,19 +2701,25 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider ec2 detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("get-config: starting")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider ec2 detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # One of the devices has no IPv4 configuration to be modified
-        nmc.pexp.expect("device has no suitable applied connection. Skip")
+        pexp.expect("device has no suitable applied connection. Skip")
         # The other one was lacking an address set it up.
-        nmc.pexp.expect("some changes were applied for provider ec2")
-        nmc.pexp.expect(pexpect.EOF)
+        pexp.expect("some changes were applied for provider ec2")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_ec2")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
         # Run nm-cloud-setup for the second time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2643,17 +2729,21 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider ec2 detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("get-config: starting")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider ec2 detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # No changes this time
-        nmc.pexp.expect('device needs no update to applied connection "con-eth0"')
-        nmc.pexp.expect("no changes were applied for provider ec2")
-        nmc.pexp.expect(pexpect.EOF)
-
-        Util.valgrind_check_log(nmc.valgrind_log, "test_ec2")
+        pexp.expect('device needs no update to applied connection "con-eth0"')
+        pexp.expect("no changes were applied for provider ec2")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_ec2")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
     @cloud_setup_test
     def test_gcp(self):
@@ -2671,7 +2761,7 @@ class TestNmCloudSetup(unittest.TestCase):
         self._mock_path(gcp_iface + "1/forwarded-ips/0", TestNmCloudSetup._ip2)
 
         # Run nm-cloud-setup for the first time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2681,20 +2771,26 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider GCP detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("found GCP interfaces: 2")
-        nmc.pexp.expect(r"GCP interface\[0]: found a requested device with hwaddr")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider GCP detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("found GCP interfaces: 2")
+        pexp.expect(r"GCP interface\[0]: found a requested device with hwaddr")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # One of the devices has no IPv4 configuration to be modified
-        nmc.pexp.expect("device has no suitable applied connection. Skip")
+        pexp.expect("device has no suitable applied connection. Skip")
         # The other one was lacking an address set it up.
-        nmc.pexp.expect("some changes were applied for provider GCP")
-        nmc.pexp.expect(pexpect.EOF)
+        pexp.expect("some changes were applied for provider GCP")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_gcp")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
         # Run nm-cloud-setup for the second time
-        nmc = Util.cmd_call_pexpect(
+        pexp = self.ctx.cmd_call_pexpect(
             ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
             [],
             {
@@ -2704,17 +2800,112 @@ class TestNmCloudSetup(unittest.TestCase):
             },
         )
 
-        nmc.pexp.expect("provider GCP detected")
-        nmc.pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
-        nmc.pexp.expect("get-config: starting")
-        nmc.pexp.expect("get-config: success")
-        nmc.pexp.expect("meta data received")
+        pexp.expect("provider GCP detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
         # No changes this time
-        nmc.pexp.expect('device needs no update to applied connection "con-eth0"')
-        nmc.pexp.expect("no changes were applied for provider GCP")
-        nmc.pexp.expect(pexpect.EOF)
+        pexp.expect('device needs no update to applied connection "con-eth0"')
+        pexp.expect("no changes were applied for provider GCP")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_gcp")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
-        Util.valgrind_check_log(nmc.valgrind_log, "test_gcp")
+    @cloud_setup_test
+    def test_oci(self):
+        self._mock_devices()
+
+        oci_meta = "/opc/v2/"
+        self._mock_path(oci_meta + "instance", "{}")
+        self._mock_path(
+            oci_meta + "vnics",
+            """
+        [
+          {
+            "macAddr": "%s",
+            "privateIp": "%s",
+            "subnetCidrBlock": "172.31.16.0/20",
+            "virtualRouterIp": "172.31.16.1",
+            "vlanTag": 810,
+            "vnicId": "ocid1.vnic.oc1.cz-adamov1.foobarbaz"
+          },
+          {
+            "macAddr": "%s",
+            "privateIp": "%s",
+            "subnetCidrBlock": "172.31.166.0/20",
+            "virtualRouterIp": "172.31.166.1",
+            "vlanTag": 700,
+            "vnicId": "ocid1.vnic.oc1.uk-hogwarts.expelliarmus"
+          }
+        ]
+        """
+            % (
+                TestNmCloudSetup._mac1,
+                TestNmCloudSetup._ip1,
+                TestNmCloudSetup._mac2,
+                TestNmCloudSetup._ip2,
+            ),
+        )
+
+        # Run nm-cloud-setup for the first time
+        pexp = self.ctx.cmd_call_pexpect(
+            ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
+            [],
+            {
+                "NM_CLOUD_SETUP_OCI_HOST": self.md_url,
+                "NM_CLOUD_SETUP_LOG": "trace",
+                "NM_CLOUD_SETUP_OCI": "yes",
+            },
+        )
+
+        pexp.expect("provider oci detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
+        # One of the devices has no IPv4 configuration to be modified
+        pexp.expect("device has no suitable applied connection. Skip")
+        # The other one was lacking an address set it up.
+        pexp.expect("some changes were applied for provider oci")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_oci")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
+
+        # Run nm-cloud-setup for the second time
+        pexp = self.ctx.cmd_call_pexpect(
+            ENV_NM_TEST_CLIENT_CLOUD_SETUP_PATH,
+            [],
+            {
+                "NM_CLOUD_SETUP_OCI_HOST": self.md_url,
+                "NM_CLOUD_SETUP_LOG": "trace",
+                "NM_CLOUD_SETUP_OCI": "yes",
+            },
+        )
+
+        pexp.expect("provider oci detected")
+        pexp.expect("found interfaces: CC:00:00:00:00:01, CC:00:00:00:00:02")
+        pexp.expect("get-config: starting")
+        pexp.expect("get-config: success")
+        pexp.expect("meta data received")
+        # No changes this time
+        pexp.expect('device needs no update to applied connection "con-eth0"')
+        pexp.expect("no changes were applied for provider oci")
+        (exitstatus, signalstatus, valgrind_log) = self.ctx.cmd_close_pexpect(pexp)
+        Util.valgrind_check_log(valgrind_log, "test_oci")
+        self.assertIsNone(
+            signalstatus,
+            "Unexpectedly got " + Util.signal_no_to_str(signalstatus or 0),
+        )
+        self.assertEqual(exitstatus, 0, "Unexpectedly returned a non-zero status")
 
 
 ###############################################################################