From e35b0ecaca0ec64ed7ecbcaebef05caaf00de230 Mon Sep 17 00:00:00 2001 From: Amadeus Date: Thu, 12 Nov 2026 14:47:06 -0300 Subject: [PATCH] Report failed upgrades with a non-zero exit status mint-refresh-cache, mintupdate-cli and system_automatic_upgrades.py all exited with 0 when the cache refresh or apt-get failed, and the log always ended with "-- Automatic Upgrade completed", so a failed automatic upgrade looked like a successful one to systemd and to any script (#1107). - mint-refresh-cache: Exit with 1 when the update fails or when it is run without admin privileges. - mintupdate-cli.py: Exit with apt-get's status. When the refresh failed the upgrade still runs on the current cache as before, but the exit status is 1 if apt-get itself succeeded. - system_automatic_upgrades.py: Log "-- Automatic Upgrade failed (exit N)" and exit with that status, so mintupdate-automation-upgrade.service is marked failed. Successful runs are unchanged. Add tests/test_exit_status.py. Co-Authored-By: Claude Opus 5.5 --- tests/test_blacklist.py | 4 +- tests/test_exit_status.py | 59 +++++++++++++++++++ usr/bin/mint-refresh-cache | 2 + .../linuxmint/mintUpdate/mintupdate-cli.py | 6 +- .../mintUpdate/system_automatic_upgrades.py | 10 +++- 5 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 tests/test_exit_status.py diff --git a/tests/test_blacklist.py b/tests/test_blacklist.py index fb299b51..862110f6 100644 --- a/tests/test_blacklist.py +++ b/tests/test_blacklist.py @@ -84,8 +84,10 @@ def exists(path): with mock.patch.object(aptUpdater, "AptUpdater", FakeAptUpdater), \ mock.patch("os.path.exists", exists), \ mock.patch.object(sys, "argv", ["mintupdate-cli"] + list(args)), \ - contextlib.redirect_stdout(output): + contextlib.redirect_stdout(output), \ + mock.patch("sys.exit") as exit: runpy.run_path(os.path.join(libPath, "mintupdate-cli.py"), run_name="__main__") + exit.assert_called_once_with(0) return [line.split()[1] for line in output.getvalue().splitlines()] class TestUpdateList(unittest.TestCase): diff --git a/tests/test_exit_status.py b/tests/test_exit_status.py new file mode 100644 index 00000000..e5268924 --- /dev/null +++ b/tests/test_exit_status.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 + +# Exit status of mintupdate-cli and mint-refresh-cache. +# Run with: /usr/bin/python3 -m unittest discover -s tests + +import os +import runpy +import subprocess +import sys +import unittest +from types import SimpleNamespace +from unittest import mock + +from test_blacklist import FakeAptUpdater, libPath, myPath +import aptUpdater + +def cli_exit_status(refresh_status, apt_status): + # Runs "mintupdate-cli upgrade --refresh-cache" on the stub packages, with + # mint-refresh-cache and apt-get replaced by the given exit statuses + real_exists = os.path.exists + def exists(path): + return False if path == "/etc/mintupdate.blacklist" else real_exists(path) + FakeAptUpdater.gui_blacklist = [] + with mock.patch.object(aptUpdater, "AptUpdater", FakeAptUpdater), \ + mock.patch("os.path.exists", exists), \ + mock.patch.object(sys, "argv", ["mintupdate-cli", "upgrade", "--refresh-cache"]), \ + mock.patch("subprocess.run", return_value=SimpleNamespace(returncode=refresh_status)), \ + mock.patch("subprocess.call", return_value=apt_status), \ + mock.patch("sys.exit") as exit: + runpy.run_path(os.path.join(libPath, "mintupdate-cli.py"), run_name="__main__") + exit.assert_called_once() + return exit.call_args.args[0] + +class TestCli(unittest.TestCase): + + def test_success(self): + self.assertEqual(cli_exit_status(0, 0), 0) + + def test_apt_get_failed(self): + self.assertEqual(cli_exit_status(0, 100), 100) + + def test_refresh_failed(self): + # The upgrade still runs on the old cache, but the failure is reported + self.assertEqual(cli_exit_status(1, 0), 1) + + def test_both_failed(self): + self.assertEqual(cli_exit_status(1, 100), 100) + +class TestRefreshCache(unittest.TestCase): + + @unittest.skipIf(os.getuid() == 0, "needs a regular user") + def test_not_root(self): + script = os.path.join(myPath, "..", "usr", "bin", "mint-refresh-cache") + result = subprocess.run([sys.executable, script], capture_output=True, text=True) + self.assertEqual(result.returncode, 1) + self.assertIn("requires admin privileges", result.stdout) + +if __name__ == "__main__": + unittest.main() diff --git a/usr/bin/mint-refresh-cache b/usr/bin/mint-refresh-cache index a68db21c..910d947a 100755 --- a/usr/bin/mint-refresh-cache +++ b/usr/bin/mint-refresh-cache @@ -11,5 +11,7 @@ if __name__ == "__main__": except Exception as e: print("Error: mint-refresh-cache could not update the cache.") print(e) + sys.exit(1) else: print("Error: mint-refresh-cache requires admin privileges") + sys.exit(1) diff --git a/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py b/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py index c06805ca..da60ef56 100755 --- a/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py +++ b/usr/lib/linuxmint/mintUpdate/mintupdate-cli.py @@ -26,9 +26,10 @@ parser.add_argument("-v", "--version", action="version", version="__DEB_VERSION__", help="Display the current version") args = parser.parse_args() + returncode = 0 try: if args.refresh_cache: - subprocess.run("sudo /usr/bin/mint-refresh-cache", shell=True) + returncode = subprocess.run("sudo /usr/bin/mint-refresh-cache", shell=True).returncode check = AptUpdater() check.load_cache() check.find_changes() @@ -81,7 +82,8 @@ arguments.append("--install-recommends") if args.keep_configuration: arguments.extend(["--option", "Dpkg::Options::=--force-confold"]) - subprocess.call(arguments + packages, env=environment) + returncode = subprocess.call(arguments + packages, env=environment) or returncode except: traceback.print_exc() sys.exit(1) + sys.exit(returncode) diff --git a/usr/lib/linuxmint/mintUpdate/system_automatic_upgrades.py b/usr/lib/linuxmint/mintUpdate/system_automatic_upgrades.py index 9a37a861..ee07469f 100755 --- a/usr/lib/linuxmint/mintUpdate/system_automatic_upgrades.py +++ b/usr/lib/linuxmint/mintUpdate/system_automatic_upgrades.py @@ -36,6 +36,7 @@ except: pass + returncode = 1 try: # Parse options file arguments = [] @@ -51,7 +52,7 @@ '--who="Update Manager"', "--what=shutdown", "--mode=block", "/usr/bin/mintupdate-cli", "upgrade", "--refresh-cache", "--yes"] cmd.extend(arguments) - subprocess.run(cmd, stdout=log, stderr=log) + returncode = subprocess.run(cmd, stdout=log, stderr=log).returncode except: import traceback @@ -64,7 +65,12 @@ except: pass - log.write("-- Automatic Upgrade completed\n") + if returncode == 0: + log.write("-- Automatic Upgrade completed\n") + else: + log.write("-- Automatic Upgrade failed (exit %d)\n" % returncode) else: log.write("-- Power supply not connected and battery below %d%%, abort automatic update.\n" % MIN_BATTERY_PERCENTAGE) + returncode = 0 log.close() +sys.exit(returncode)