Compare commits
8 Commits
v0.36.1
...
4bbc0fa97e
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bbc0fa97e | |||
|
a0f8055328
|
|||
| 323451428b | |||
|
479189ee4e
|
|||
| 51133e4042 | |||
|
bcf6ac2656
|
|||
| d59261f021 | |||
|
44923b771a
|
@@ -5,6 +5,25 @@ All notable changes to RigDoctor are recorded here. Format follows
|
|||||||
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
||||||
release tag (so the auto-updater, D18, can compare versions).
|
release tag (so the auto-updater, D18, can compare versions).
|
||||||
|
|
||||||
|
## [0.37.1] - 2026-05-22
|
||||||
|
### Fixed
|
||||||
|
- **`rigdoctor update` now uses the right method for how RigDoctor was installed.** It detects
|
||||||
|
apt (`.deb`), pip (venv/`.run`), or source installs (`updates.install_kind()`); only pip
|
||||||
|
installs self-update in place. An apt install no longer fails with "No module named pip" —
|
||||||
|
it (and the GUI Update button) shows `sudo apt update && sudo apt install --only-upgrade
|
||||||
|
rigdoctor`; a source checkout points to `git pull`.
|
||||||
|
|
||||||
|
## [0.37.0] - 2026-05-22
|
||||||
|
### Added
|
||||||
|
- **Version footer** — a footer across the bottom of the window shows `RigDoctor v<version>` in
|
||||||
|
the bottom-right (moved out of the sidebar).
|
||||||
|
### Fixed
|
||||||
|
- **Pages scroll when content doesn't fit, and the window is no longer pinned to the tallest
|
||||||
|
page's height.** Long pages (Settings, Tuning, …) get a scrollbar when too tall — so controls
|
||||||
|
like Uninstall are always reachable — and the window can now be resized smaller than the screen
|
||||||
|
(min height dropped from "taller than the screen" to ~600px). Pages that manage their own
|
||||||
|
scroll/fill (Dashboard, System Health, Inventory, Share) are unchanged.
|
||||||
|
|
||||||
## [0.36.1] - 2026-05-22
|
## [0.36.1] - 2026-05-22
|
||||||
### Fixed
|
### Fixed
|
||||||
- `rigdoctor gui` printed the wrong fix when PySide6 is missing — it suggested the non-existent
|
- `rigdoctor gui` printed the wrong fix when PySide6 is missing — it suggested the non-existent
|
||||||
|
|||||||
@@ -29,6 +29,16 @@ freeze are usually lost. RigDoctor pulls it together and keeps the evidence.
|
|||||||
or share a live **terminal session** for remote help.
|
or share a live **terminal session** for remote help.
|
||||||
- **Self-updating** — `apt upgrade`, or the in-app updater.
|
- **Self-updating** — `apt upgrade`, or the in-app updater.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| Dashboard | Inventory |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
|
||||||
|
**Share** — a read-only or interactive terminal session over the relay, for remote help:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
### Debian / Ubuntu — `.deb`
|
### Debian / Ubuntu — `.deb`
|
||||||
@@ -41,24 +51,15 @@ apt pulls the GUI dependencies (PySide6, pyte) automatically:
|
|||||||
sudo apt install ./rigdoctor_*_all.deb # CLI only: add --no-install-recommends
|
sudo apt install ./rigdoctor_*_all.deb # CLI only: add --no-install-recommends
|
||||||
```
|
```
|
||||||
|
|
||||||
**Or add the apt repository** for `apt install` + automatic updates. The registry is private and
|
**Or add the apt repository** for `apt install` + automatic updates. The registry is public and
|
||||||
GPG-signed, so you need a Gitea token with **`read:package`**, the signing key, and the deb822
|
GPG-signed — no token needed; just add the signing key and a deb822 source:
|
||||||
source (`read -rsp` keeps the token out of your shell history):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
read -rsp 'Gitea read:package token: ' TOKEN; echo
|
# signing key → dearmored into the keyring
|
||||||
|
|
||||||
# signing key → dearmored into the keyring (the key endpoint requires the token too)
|
|
||||||
sudo install -d -m 0755 /etc/apt/keyrings
|
sudo install -d -m 0755 /etc/apt/keyrings
|
||||||
curl -fsSL --user <user>:"$TOKEN" \
|
curl -fsSL https://git.jesseyvanofferen.com/api/packages/jessey/debian/repository.key \
|
||||||
https://git.jesseyvanofferen.com/api/packages/jessey/debian/repository.key \
|
|
||||||
| sudo gpg --dearmor -o /etc/apt/keyrings/gitea-jessey.gpg
|
| sudo gpg --dearmor -o /etc/apt/keyrings/gitea-jessey.gpg
|
||||||
|
|
||||||
# download credentials, kept out of the sources file
|
|
||||||
printf 'machine git.jesseyvanofferen.com login <user> password %s\n' "$TOKEN" \
|
|
||||||
| sudo tee /etc/apt/auth.conf.d/rigdoctor.conf >/dev/null
|
|
||||||
sudo chmod 0600 /etc/apt/auth.conf.d/rigdoctor.conf
|
|
||||||
|
|
||||||
# the source (modern deb822 format, GPG-verified, all-arch)
|
# the source (modern deb822 format, GPG-verified, all-arch)
|
||||||
sudo tee /etc/apt/sources.list.d/rigdoctor.sources >/dev/null <<'EOF'
|
sudo tee /etc/apt/sources.list.d/rigdoctor.sources >/dev/null <<'EOF'
|
||||||
Types: deb
|
Types: deb
|
||||||
@@ -72,8 +73,7 @@ EOF
|
|||||||
sudo apt update && sudo apt install rigdoctor
|
sudo apt update && sudo apt install rigdoctor
|
||||||
```
|
```
|
||||||
|
|
||||||
Then `sudo apt upgrade` keeps it current. *(Quick-and-dirty alternative if the registry isn't
|
Then `sudo apt upgrade` keeps it current.
|
||||||
signed: skip the key and use a one-line `deb [arch=all trusted=yes] …/debian stable main` source.)*
|
|
||||||
|
|
||||||
### Any distro — self-extracting `.run` (no root)
|
### Any distro — self-extracting `.run` (no root)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rigdoctor"
|
name = "rigdoctor"
|
||||||
version = "0.36.1"
|
version = "0.37.1"
|
||||||
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||||
|
|
||||||
__version__ = "0.36.1"
|
__version__ = "0.37.1"
|
||||||
|
|||||||
@@ -263,6 +263,10 @@ def cmd_update(args) -> int:
|
|||||||
print("\nWhat's new:\n" + "\n".join(" " + ln for ln in notes.splitlines()) + "\n")
|
print("\nWhat's new:\n" + "\n".join(" " + ln for ln in notes.splitlines()) + "\n")
|
||||||
if args.check:
|
if args.check:
|
||||||
return 0
|
return 0
|
||||||
|
kind = updates.install_kind()
|
||||||
|
if kind != "pip": # apt/source installs aren't pip-updatable — show the right command
|
||||||
|
print(updates.update_hint(kind))
|
||||||
|
return 0
|
||||||
print(f"Installing {tag}…")
|
print(f"Installing {tag}…")
|
||||||
rc, out = updates.apply_update(tag)
|
rc, out = updates.apply_update(tag)
|
||||||
print(out[-2000:])
|
print(out[-2000:])
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ state for the UI; `apply_update` performs the no-root self-update.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
import json
|
import json
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import urllib.error
|
import urllib.error
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..config import load_token
|
from ..config import load_token
|
||||||
@@ -31,6 +34,50 @@ UP_TO_DATE = "up-to-date"
|
|||||||
AVAILABLE = "available"
|
AVAILABLE = "available"
|
||||||
|
|
||||||
|
|
||||||
|
APT_PACKAGE = "rigdoctor"
|
||||||
|
|
||||||
|
|
||||||
|
def _dpkg_owns(path: Path) -> bool:
|
||||||
|
"""True if dpkg reports `path` belongs to a package (i.e. an apt/.deb install)."""
|
||||||
|
if not shutil.which("dpkg"):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
r = subprocess.run(["dpkg", "-S", str(path)], capture_output=True, text=True, timeout=5)
|
||||||
|
except (subprocess.SubprocessError, OSError):
|
||||||
|
return False
|
||||||
|
return r.returncode == 0 and APT_PACKAGE in r.stdout
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=1)
|
||||||
|
def install_kind() -> str:
|
||||||
|
"""How RigDoctor was installed: 'apt' (.deb), 'pip' (venv/.run), or 'dev' (source checkout).
|
||||||
|
|
||||||
|
Decides which updater to use: only 'pip' can self-update in place; apt is root/dpkg-managed
|
||||||
|
and source is VCS-managed, so those are guided rather than auto-applied.
|
||||||
|
"""
|
||||||
|
pkg = Path(__file__).resolve().parents[1] # .../rigdoctor
|
||||||
|
if _dpkg_owns(pkg / "__init__.py"):
|
||||||
|
return "apt"
|
||||||
|
if sys.prefix != sys.base_prefix: # inside a venv → the pip/.run install
|
||||||
|
return "pip"
|
||||||
|
if (pkg.parents[1] / "pyproject.toml").exists(): # repo checkout
|
||||||
|
return "dev"
|
||||||
|
if str(pkg).startswith("/usr/") or "/dist-packages/" in str(pkg):
|
||||||
|
return "apt" # system-managed but no dpkg record — still don't pip
|
||||||
|
return "pip"
|
||||||
|
|
||||||
|
|
||||||
|
def update_hint(kind: str | None = None) -> str:
|
||||||
|
"""Human guidance for installs that can't self-update via pip (apt / source)."""
|
||||||
|
kind = kind or install_kind()
|
||||||
|
if kind == "apt":
|
||||||
|
return ("Installed via apt — update with:\n"
|
||||||
|
f" sudo apt update && sudo apt install --only-upgrade {APT_PACKAGE}")
|
||||||
|
if kind == "dev":
|
||||||
|
return "Running from a source checkout — update with `git pull`."
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _parse(version: str) -> tuple[int, ...]:
|
def _parse(version: str) -> tuple[int, ...]:
|
||||||
return tuple(int(p) for p in version.lstrip("vV").split(".") if p.isdigit())
|
return tuple(int(p) for p in version.lstrip("vV").split(".") if p.isdigit())
|
||||||
|
|
||||||
@@ -100,11 +147,16 @@ def list_releases(limit: int = 15, timeout: float = 6.0) -> tuple[list[tuple[str
|
|||||||
|
|
||||||
|
|
||||||
def apply_update(tag: str) -> tuple[int, str]:
|
def apply_update(tag: str) -> tuple[int, str]:
|
||||||
"""Self-update the current (user-local) install to `tag` via authenticated pip.
|
"""Update to `tag` using the method matching how RigDoctor was installed.
|
||||||
|
|
||||||
Installs `rigdoctor[gui] @ git+https://oauth2:<token>@…/rigdoctor.git@<tag>` into
|
Only pip/venv installs are upgraded in place (authenticated pip install of
|
||||||
the running environment. Returns (exit_code, output) with the token scrubbed.
|
`rigdoctor[gui] @ git+https://oauth2:<token>@…/rigdoctor.git@<tag>`). apt and source
|
||||||
|
installs can't be (root/dpkg- or VCS-managed), so they return guidance instead of
|
||||||
|
attempting pip. Returns (exit_code, output) with the token scrubbed.
|
||||||
"""
|
"""
|
||||||
|
kind = install_kind()
|
||||||
|
if kind != "pip":
|
||||||
|
return (1, update_hint(kind))
|
||||||
token = load_token()
|
token = load_token()
|
||||||
if not token:
|
if not token:
|
||||||
return (1, "No update token configured. Run `rigdoctor login`.")
|
return (1, "No update token configured. Run `rigdoctor login`.")
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
|
|||||||
QMainWindow,
|
QMainWindow,
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
|
QScrollArea,
|
||||||
QStackedWidget,
|
QStackedWidget,
|
||||||
QSystemTrayIcon,
|
QSystemTrayIcon,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
@@ -51,6 +52,10 @@ _NAV = [
|
|||||||
("App", ["Settings", "Share"]),
|
("App", ["Settings", "Share"]),
|
||||||
]
|
]
|
||||||
_PAGES = [name for _section, names in _NAV for name in names]
|
_PAGES = [name for _section, names in _NAV for name in names]
|
||||||
|
# Pages that manage their own scrolling (pinned header + inner scroll) or must fill the
|
||||||
|
# viewport (the Share terminal) — these are added to the stack as-is; every other page is
|
||||||
|
# wrapped in a QScrollArea so it scrolls when too tall and doesn't pin the window's height.
|
||||||
|
_NO_WRAP = {"Dashboard", "System Health", "Inventory", "Share"}
|
||||||
_ICON = Path(__file__).parent / "assets" / "rigdoctor.svg"
|
_ICON = Path(__file__).parent / "assets" / "rigdoctor.svg"
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +73,11 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
central = QWidget()
|
central = QWidget()
|
||||||
self.setCentralWidget(central)
|
self.setCentralWidget(central)
|
||||||
layout = QHBoxLayout(central)
|
outer = QVBoxLayout(central)
|
||||||
|
outer.setContentsMargins(0, 0, 0, 0)
|
||||||
|
outer.setSpacing(0)
|
||||||
|
body = QWidget()
|
||||||
|
layout = QHBoxLayout(body)
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
layout.setSpacing(0)
|
layout.setSpacing(0)
|
||||||
|
|
||||||
@@ -100,11 +109,14 @@ class MainWindow(QMainWindow):
|
|||||||
"Share": self.share_page,
|
"Share": self.share_page,
|
||||||
}
|
}
|
||||||
for name in _PAGES:
|
for name in _PAGES:
|
||||||
self._stack.addWidget(self._pages[name])
|
page = self._pages[name]
|
||||||
|
self._stack.addWidget(page if name in _NO_WRAP else self._scrollable(page))
|
||||||
content_layout.addWidget(self._stack)
|
content_layout.addWidget(self._stack)
|
||||||
|
|
||||||
layout.addWidget(self._build_sidebar())
|
layout.addWidget(self._build_sidebar())
|
||||||
layout.addWidget(content, 1)
|
layout.addWidget(content, 1)
|
||||||
|
outer.addWidget(body, 1)
|
||||||
|
outer.addWidget(self._build_footer())
|
||||||
|
|
||||||
self._worker = SamplerWorker(interval=interval)
|
self._worker = SamplerWorker(interval=interval)
|
||||||
self._worker.sampled.connect(self.dashboard.update_sample)
|
self._worker.sampled.connect(self.dashboard.update_sample)
|
||||||
@@ -216,9 +228,6 @@ class MainWindow(QMainWindow):
|
|||||||
v.addStretch(1)
|
v.addStretch(1)
|
||||||
live = QLabel(f'<span style="color:{ACCENT};">●</span> <span style="color:{MUTED};">Live</span>')
|
live = QLabel(f'<span style="color:{ACCENT};">●</span> <span style="color:{MUTED};">Live</span>')
|
||||||
v.addWidget(live)
|
v.addWidget(live)
|
||||||
version = QLabel(f"v{__version__}")
|
|
||||||
version.setObjectName("Muted")
|
|
||||||
v.addWidget(version)
|
|
||||||
changelog_btn = QPushButton("Changelog")
|
changelog_btn = QPushButton("Changelog")
|
||||||
changelog_btn.setObjectName("LinkButton")
|
changelog_btn.setObjectName("LinkButton")
|
||||||
changelog_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
changelog_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||||
@@ -248,6 +257,27 @@ class MainWindow(QMainWindow):
|
|||||||
v.addWidget(self._restart_btn)
|
v.addWidget(self._restart_btn)
|
||||||
return bar
|
return bar
|
||||||
|
|
||||||
|
def _scrollable(self, page: QWidget) -> QScrollArea:
|
||||||
|
"""Wrap a page so it scrolls when taller than the window — and so the window can shrink
|
||||||
|
below the page's natural height instead of being pinned to it."""
|
||||||
|
area = QScrollArea()
|
||||||
|
area.setWidget(page)
|
||||||
|
area.setWidgetResizable(True)
|
||||||
|
area.setFrameShape(QFrame.Shape.NoFrame)
|
||||||
|
area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
return area
|
||||||
|
|
||||||
|
def _build_footer(self) -> QFrame:
|
||||||
|
bar = QFrame()
|
||||||
|
bar.setObjectName("Footer")
|
||||||
|
h = QHBoxLayout(bar)
|
||||||
|
h.setContentsMargins(14, 5, 16, 5)
|
||||||
|
h.addStretch(1)
|
||||||
|
version = QLabel(f"RigDoctor v{__version__}")
|
||||||
|
version.setObjectName("Muted")
|
||||||
|
h.addWidget(version)
|
||||||
|
return bar
|
||||||
|
|
||||||
def _restart(self) -> None:
|
def _restart(self) -> None:
|
||||||
gui = os.path.join(os.path.dirname(sys.executable), "rigdoctor-gui")
|
gui = os.path.join(os.path.dirname(sys.executable), "rigdoctor-gui")
|
||||||
if os.path.exists(gui):
|
if os.path.exists(gui):
|
||||||
@@ -259,6 +289,9 @@ class MainWindow(QMainWindow):
|
|||||||
def _apply_update(self) -> None:
|
def _apply_update(self) -> None:
|
||||||
if not self._latest_tag:
|
if not self._latest_tag:
|
||||||
return
|
return
|
||||||
|
if updates.install_kind() != "pip": # apt/source: can't pip-update — show the command
|
||||||
|
QMessageBox.information(self, "Update RigDoctor", updates.update_hint())
|
||||||
|
return
|
||||||
box = QMessageBox(self)
|
box = QMessageBox(self)
|
||||||
box.setWindowTitle(f"Update to {self._latest_tag}")
|
box.setWindowTitle(f"Update to {self._latest_tag}")
|
||||||
box.setText(f"Update RigDoctor to {self._latest_tag}?")
|
box.setText(f"Update RigDoctor to {self._latest_tag}?")
|
||||||
@@ -424,7 +457,7 @@ class MainWindow(QMainWindow):
|
|||||||
self._update_label.setText("update check unavailable")
|
self._update_label.setText("update check unavailable")
|
||||||
elif state == updates.AVAILABLE:
|
elif state == updates.AVAILABLE:
|
||||||
self._update_label.setText(f'<span style="color:{GOOD};">{tag} available</span>')
|
self._update_label.setText(f'<span style="color:{GOOD};">{tag} available</span>')
|
||||||
self._update_btn.setText(f"Update to {tag}")
|
self._update_btn.setText(f"Update to {tag}" if updates.install_kind() == "pip" else "How to update")
|
||||||
self._update_btn.setVisible(True)
|
self._update_btn.setVisible(True)
|
||||||
if self._alert_monitor.enabled and tag != self._notified_update_tag:
|
if self._alert_monitor.enabled and tag != self._notified_update_tag:
|
||||||
self._notified_update_tag = tag # once per version, not every poll
|
self._notified_update_tag = tag # once per version, not every poll
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ QMainWindow, #ContentArea, #Page {{ background: {BG}; }}
|
|||||||
QLabel {{ background: transparent; }}
|
QLabel {{ background: transparent; }}
|
||||||
|
|
||||||
#Sidebar {{ background: {SIDEBAR}; border-right: 1px solid {CARD_BORDER}; }}
|
#Sidebar {{ background: {SIDEBAR}; border-right: 1px solid {CARD_BORDER}; }}
|
||||||
|
#Footer {{ background: {SIDEBAR}; border-top: 1px solid {CARD_BORDER}; }}
|
||||||
|
#Footer QLabel {{ font-size: 11px; }}
|
||||||
#AppTitle {{ font-size: 17px; font-weight: 800; }}
|
#AppTitle {{ font-size: 17px; font-weight: 800; }}
|
||||||
#AppSubtitle {{ color: {MUTED}; font-size: 11px; }}
|
#AppSubtitle {{ color: {MUTED}; font-size: 11px; }}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
"""Tests for the M13 updater: install detection + routing the update to the right method."""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from rigdoctor.core import updates
|
||||||
|
|
||||||
|
|
||||||
|
class InstallKindTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
updates.install_kind.cache_clear()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
updates.install_kind.cache_clear()
|
||||||
|
|
||||||
|
def test_apt_when_dpkg_owns_the_package(self):
|
||||||
|
with mock.patch.object(updates, "_dpkg_owns", return_value=True):
|
||||||
|
self.assertEqual(updates.install_kind(), "apt")
|
||||||
|
|
||||||
|
def test_pip_when_running_in_a_venv(self):
|
||||||
|
with mock.patch.object(updates, "_dpkg_owns", return_value=False), \
|
||||||
|
mock.patch.object(updates.sys, "prefix", "/opt/venv"), \
|
||||||
|
mock.patch.object(updates.sys, "base_prefix", "/usr"):
|
||||||
|
self.assertEqual(updates.install_kind(), "pip")
|
||||||
|
|
||||||
|
|
||||||
|
class ApplyUpdateRoutingTests(unittest.TestCase):
|
||||||
|
def test_apt_returns_guidance_and_never_runs_pip(self):
|
||||||
|
with mock.patch.object(updates, "install_kind", return_value="apt"), \
|
||||||
|
mock.patch("subprocess.run") as run:
|
||||||
|
rc, out = updates.apply_update("v9.9.9")
|
||||||
|
self.assertEqual(rc, 1)
|
||||||
|
self.assertIn("apt install --only-upgrade", out)
|
||||||
|
run.assert_not_called()
|
||||||
|
|
||||||
|
def test_dev_returns_guidance_and_never_runs_pip(self):
|
||||||
|
with mock.patch.object(updates, "install_kind", return_value="dev"), \
|
||||||
|
mock.patch("subprocess.run") as run:
|
||||||
|
rc, out = updates.apply_update("v9.9.9")
|
||||||
|
self.assertIn("git pull", out)
|
||||||
|
run.assert_not_called()
|
||||||
|
|
||||||
|
def test_pip_install_runs_pip(self):
|
||||||
|
proc = mock.Mock(returncode=0, stdout="Successfully installed", stderr="")
|
||||||
|
with mock.patch.object(updates, "install_kind", return_value="pip"), \
|
||||||
|
mock.patch.object(updates, "load_token", return_value="TOK"), \
|
||||||
|
mock.patch("subprocess.run", return_value=proc) as run:
|
||||||
|
rc, _out = updates.apply_update("v1.2.3")
|
||||||
|
self.assertEqual(rc, 0)
|
||||||
|
cmd = run.call_args[0][0]
|
||||||
|
self.assertIn("pip", cmd)
|
||||||
|
self.assertIn("install", cmd)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateHintTests(unittest.TestCase):
|
||||||
|
def test_apt_hint_names_the_apt_command(self):
|
||||||
|
self.assertIn("apt install --only-upgrade rigdoctor", updates.update_hint("apt"))
|
||||||
|
|
||||||
|
def test_dev_hint_says_git_pull(self):
|
||||||
|
self.assertIn("git pull", updates.update_hint("dev"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user