Update ohne Neustart, wenn nur Oberflaechendateien wechseln
Bisher verlangte jedes Update einen Home-Assistant-Neustart. Noetig ist er aber nur fuer Python-Code: HA importiert die Module einmal beim Start und haengt danach mit lebenden Objekten daran. frontend/ dagegen wird direkt von der Platte ausgeliefert (StaticPathConfig, cache_headers=False) - eine ersetzte .js ist sofort wirksam, es braucht nur ein Neuladen im Browser. aktualisierung.neustart_noetig() vergleicht die alte gegen die neue Fassung ueber sha256 je Datei und meldet "kein Neustart" nur, wenn JEDE Abweichung unter frontend/ liegt oder die manifest.json ist. Alles andere - .py, services.yaml, translations/, vorlage/ - gilt als neustartpflichtig, auch wo es das im Einzelfall nicht waere. Die Schieflage ist Absicht: ein faelschlich ausgelassener Neustart laesst neuen Python-Code nie anlaufen, und der Fehler wird woanders gesucht. manifest.json ist ausgenommen, weil sich seine Versionsnummer bei jeder Veroeffentlichung aendert - sonst waere die Unterscheidung wertlos. Weil HA das Manifest fuer die Laufzeit festhaelt (loader.py: hass.data[ DATA_INTEGRATIONS]), liest version_von_platte() die Nummer direkt von der Platte; koordinator.version_neu_lesen() zieht sie nach einem Update ohne Neustart nach, damit Panel und App die neue Fassung auch anzeigen. Panel und App zeigen im Neustart-freien Fall "Seite neu laden" statt "Installation abschliessen - Jetzt neu starten". Neun neue Testfaelle fuer die Unterscheidung, 23 Tests in der Aktualisierungs-Suite gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -216,9 +216,18 @@ export interface IntegrationUpdateAngabe {
|
||||
verfuegbar: boolean;
|
||||
version: string | null;
|
||||
geprueft_am: string;
|
||||
/** true unmittelbar nach einer erfolgreichen Installation - dann ist ein
|
||||
Neustart von Home Assistant nötig, damit die neue Fassung geladen wird. */
|
||||
/** true unmittelbar nach einer erfolgreichen Installation. */
|
||||
installiert: boolean;
|
||||
/** Ob nach der Installation wirklich ein Home-Assistant-Neustart nötig ist.
|
||||
|
||||
Der Updater entscheidet das am Datei-Vergleich: betrifft das Update nur
|
||||
`frontend/`, reicht ein Neuladen der Seite - die Oberflächendateien
|
||||
werden direkt von der Platte ausgeliefert. Nur geänderter Python-Code
|
||||
braucht einen Neustart, weil Home Assistant seine Module einmal beim
|
||||
Start importiert.
|
||||
|
||||
Fehlt das Feld (ältere Integration), gilt "Neustart nötig". */
|
||||
neustart_noetig?: boolean;
|
||||
fehler: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -796,6 +796,21 @@ export function Einstellungen({
|
||||
Home Assistant startet neu …
|
||||
</span>
|
||||
</div>
|
||||
) : integrationUpdate?.installiert && integrationUpdate.neustart_noetig === false ? (
|
||||
/* Nur Oberflächendateien geändert - die liegen als statische Dateien
|
||||
auf der Platte und werden ohne lange Cache-Vorgabe ausgeliefert.
|
||||
Ein Neuladen genügt, Home Assistant muss nicht neu starten. */
|
||||
<>
|
||||
<p className="dm-erklaerung">
|
||||
Version {integrationUpdate.version} installiert - nur Oberflächendateien, kein
|
||||
Neustart nötig.
|
||||
</p>
|
||||
<div className="dm-knopfreihe">
|
||||
<ActionButton variant="primary" onClick={() => window.location.reload()}>
|
||||
App neu laden
|
||||
</ActionButton>
|
||||
</div>
|
||||
</>
|
||||
) : integrationUpdate?.installiert ? (
|
||||
<>
|
||||
<p className="dm-erklaerung">
|
||||
|
||||
@@ -42,6 +42,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -178,6 +179,112 @@ async def version_pruefen(hass: HomeAssistant, token: str, eigene_version: str)
|
||||
}
|
||||
|
||||
|
||||
# Dateien, deren Aenderung KEINEN Neustart braucht.
|
||||
#
|
||||
# frontend/ wird direkt von der Platte ausgeliefert (StaticPathConfig mit
|
||||
# cache_headers=False, siehe __init__.py) - eine ersetzte .js oder .css ist
|
||||
# sofort wirksam, es braucht nur ein Neuladen im Browser. Das gilt auch fuer
|
||||
# frontend/app/, das OTA-Buendel der Companion-App.
|
||||
#
|
||||
# manifest.json steht dabei, weil sich seine Versionsnummer bei JEDER
|
||||
# Veroeffentlichung aendert - ohne diese Ausnahme waere jedes Update ein
|
||||
# Neustart-Update, und die Unterscheidung waere wertlos. Home Assistant haelt
|
||||
# das Manifest fuer die Laufzeit fest (loader.py: hass.data[DATA_INTEGRATIONS]),
|
||||
# die von HA selbst angezeigte Versionsnummer bleibt also bis zum naechsten
|
||||
# Neustart die alte. Das ist kosmetisch; unsere eigene Anzeige liest die
|
||||
# Version direkt von der Platte (siehe __init__.py).
|
||||
OHNE_NEUSTART = ("frontend/",)
|
||||
OHNE_NEUSTART_DATEIEN = ("manifest.json",)
|
||||
|
||||
|
||||
def version_von_platte() -> str | None:
|
||||
"""Die Versionsnummer aus der manifest.json neben diesem Modul.
|
||||
|
||||
Home Assistant haelt das Manifest fuer die Laufzeit fest (loader.py:
|
||||
`cache = hass.data[DATA_INTEGRATIONS]`) - nach einem Update meldet
|
||||
`async_get_integration()` also weiter die alte Nummer, selbst wenn der
|
||||
Config-Eintrag neu geladen wird. Fuer ein Update ohne Neustart braucht es
|
||||
deshalb eine Quelle, die wirklich von der Platte liest.
|
||||
|
||||
`None`, wenn die Datei fehlt oder unlesbar ist - dann bleibt der Aufrufer
|
||||
bei dem Wert, den er schon hat, statt eine erfundene Nummer zu zeigen."""
|
||||
pfad = os.path.join(INTEGRATIONSORDNER, "manifest.json")
|
||||
try:
|
||||
with open(pfad, encoding="utf-8") as datei:
|
||||
return json.load(datei).get("version") or None
|
||||
except (OSError, ValueError) as fehler:
|
||||
_LOGGER.warning("manifest.json nicht lesbar (%s)", fehler)
|
||||
return None
|
||||
|
||||
|
||||
def _dateihashes(ordner: str) -> dict[str, str]:
|
||||
"""Alle Dateien unter `ordner` als {relativer Pfad: sha256}.
|
||||
|
||||
__pycache__ bleibt aussen vor: es entsteht beim Laufen und sagt nichts
|
||||
ueber die ausgelieferte Fassung."""
|
||||
aus: dict[str, str] = {}
|
||||
for wurzel, ordnerliste, dateien in os.walk(ordner):
|
||||
ordnerliste[:] = [o for o in ordnerliste if o != "__pycache__"]
|
||||
for name in dateien:
|
||||
pfad = os.path.join(wurzel, name)
|
||||
rel = os.path.relpath(pfad, ordner).replace(os.sep, "/")
|
||||
hasher = hashlib.sha256()
|
||||
with open(pfad, "rb") as datei:
|
||||
for block in iter(lambda: datei.read(65536), b""):
|
||||
hasher.update(block)
|
||||
aus[rel] = hasher.hexdigest()
|
||||
return aus
|
||||
|
||||
|
||||
def neustart_noetig(alter_ordner: str, neuer_ordner: str) -> bool:
|
||||
"""Ob die neue Fassung einen Home-Assistant-Neustart braucht.
|
||||
|
||||
Bewusst pessimistisch: gemeldet wird `False` nur, wenn JEDE geaenderte,
|
||||
hinzugekommene oder entfallene Datei unter frontend/ liegt oder die
|
||||
manifest.json ist. Alles andere - .py, services.yaml, translations/,
|
||||
vorlage/ - gilt als neustartpflichtig, auch wenn es das im Einzelfall
|
||||
vielleicht nicht waere.
|
||||
|
||||
Der Grund fuer die Schieflage: sagt diese Funktion faelschlich "kein
|
||||
Neustart noetig", laeuft neuer Python-Code nie an, und der Fehler wird an
|
||||
einer ganz anderen Stelle gesucht. Ein ueberfluessiger Neustart kostet
|
||||
dagegen eine Minute. Im Zweifel also Neustart.
|
||||
|
||||
Laesst sich der alte Ordner nicht lesen, gilt ebenfalls Neustart."""
|
||||
try:
|
||||
alt = _dateihashes(alter_ordner)
|
||||
neu = _dateihashes(neuer_ordner)
|
||||
except OSError as fehler:
|
||||
_LOGGER.warning(
|
||||
"Konnte alte und neue Fassung nicht vergleichen (%s) - Neustart angenommen.",
|
||||
fehler,
|
||||
)
|
||||
return True
|
||||
|
||||
geaendert = {
|
||||
pfad
|
||||
for pfad in set(alt) | set(neu)
|
||||
if alt.get(pfad) != neu.get(pfad)
|
||||
}
|
||||
ohne_neustart = {
|
||||
pfad
|
||||
for pfad in geaendert
|
||||
if pfad.startswith(OHNE_NEUSTART) or pfad in OHNE_NEUSTART_DATEIEN
|
||||
}
|
||||
rest = sorted(geaendert - ohne_neustart)
|
||||
if rest:
|
||||
_LOGGER.info(
|
||||
"Update braucht einen Neustart - geaendert ausserhalb von frontend/: %s",
|
||||
", ".join(rest[:8]) + (" …" if len(rest) > 8 else ""),
|
||||
)
|
||||
return True
|
||||
_LOGGER.info(
|
||||
"Update betrifft nur die Oberflaeche (%s Datei(en)) - kein Neustart noetig.",
|
||||
len(geaendert),
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
async def update_installieren(hass: HomeAssistant, token: str) -> dict[str, Any]:
|
||||
"""Lädt das komplette Repo-Archiv und delegiert Entpacken/Prüfen/Tauschen
|
||||
an eine blockierende Funktion im Executor - Datei- und Zip-Operationen
|
||||
@@ -202,10 +309,10 @@ async def update_installieren(hass: HomeAssistant, token: str) -> dict[str, Any]
|
||||
except Exception as fehler:
|
||||
raise AktualisierungsFehler(f"Gitea nicht erreichbar: {fehler}") from fehler
|
||||
|
||||
neue_version = await hass.async_add_executor_job(
|
||||
ergebnis = await hass.async_add_executor_job(
|
||||
entpacken_pruefen_tauschen, zip_bytes, INTEGRATIONSORDNER, _STAGING_ORDNER, _BACKUP_ORDNER
|
||||
)
|
||||
return {"version": neue_version}
|
||||
return ergebnis
|
||||
|
||||
|
||||
def entpacken_pruefen_tauschen(
|
||||
@@ -277,6 +384,10 @@ def entpacken_pruefen_tauschen(
|
||||
if "__pycache__" in ordner:
|
||||
shutil.rmtree(os.path.join(wurzel, "__pycache__"), ignore_errors=True)
|
||||
|
||||
# Vergleich VOR dem Tausch - danach gibt es die alte Fassung unter diesem
|
||||
# Pfad nicht mehr.
|
||||
neustart = neustart_noetig(integrationsordner, staging_ordner)
|
||||
|
||||
if os.path.isdir(backup_ordner):
|
||||
shutil.rmtree(backup_ordner)
|
||||
try:
|
||||
@@ -315,4 +426,4 @@ def entpacken_pruefen_tauschen(
|
||||
f"Tausch fehlgeschlagen, alte Fassung wiederhergestellt: {fehler}"
|
||||
) from fehler
|
||||
|
||||
return neue_version
|
||||
return {"version": neue_version, "neustart_noetig": neustart}
|
||||
|
||||
@@ -344,23 +344,41 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
||||
await k.app_version_veroeffentlichen()
|
||||
|
||||
async def update_installieren(_aufruf: ServiceCall) -> None:
|
||||
"""Lädt die neueste Fassung von Gitea und ersetzt den
|
||||
Integrationsordner. Home Assistant muss danach von Hand neu
|
||||
gestartet werden - kein automatischer Reload aus dem eigenen,
|
||||
noch laufenden Aufruf heraus, siehe aktualisierung.py."""
|
||||
"""Laedt die neueste Fassung von Gitea und ersetzt den
|
||||
Integrationsordner.
|
||||
|
||||
Ob danach ein Neustart noetig ist, entscheidet der Updater am
|
||||
Datei-Vergleich (aktualisierung.neustart_noetig): betrifft das Update
|
||||
nur frontend/, reicht ein Neuladen der Seite - die Oberflaechen-Dateien
|
||||
werden direkt von der Platte ausgeliefert (StaticPathConfig mit
|
||||
cache_headers=False). Nur geaenderter Python-Code braucht wirklich
|
||||
einen Neustart, weil Home Assistant seine Module einmal beim Start
|
||||
importiert und danach mit lebenden Objekten daran haengt.
|
||||
|
||||
Kein automatischer Reload aus dem eigenen, noch laufenden Aufruf
|
||||
heraus - siehe aktualisierung.py."""
|
||||
token = k.entry.options.get(CONF_GITEA_TOKEN, "")
|
||||
jetzt = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
try:
|
||||
ergebnis = await aktualisierung.update_installieren(hass, token)
|
||||
neustart = bool(ergebnis.get("neustart_noetig", True))
|
||||
_LOGGER.info(
|
||||
"Update installiert: Version %s - Home Assistant muss neu gestartet werden.",
|
||||
"Update installiert: Version %s - %s",
|
||||
ergebnis["version"],
|
||||
"Home Assistant muss neu gestartet werden."
|
||||
if neustart
|
||||
else "nur Oberflaechen-Dateien, ein Neuladen der Seite genuegt.",
|
||||
)
|
||||
if not neustart:
|
||||
# Ohne Neustart bleibt Home Assistants zwischengespeichertes
|
||||
# Manifest stehen - die neue Nummer muss von der Platte kommen.
|
||||
await k.version_neu_lesen()
|
||||
k.letzte_update_pruefung = {
|
||||
"verfuegbar": False,
|
||||
"version": ergebnis["version"],
|
||||
"geprueft_am": jetzt,
|
||||
"installiert": True,
|
||||
"neustart_noetig": neustart,
|
||||
"fehler": None,
|
||||
}
|
||||
except aktualisierung.AktualisierungsFehler as fehler:
|
||||
@@ -370,6 +388,7 @@ def registrieren(hass: HomeAssistant, k: Koordinator) -> None:
|
||||
"version": (k.letzte_update_pruefung or {}).get("version"),
|
||||
"geprueft_am": jetzt,
|
||||
"installiert": False,
|
||||
"neustart_noetig": True,
|
||||
"fehler": str(fehler),
|
||||
}
|
||||
await k.app_version_veroeffentlichen()
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"version":"2026.9.5.22","sha256":"a83dcedbf835def7fedebdb55e136ec5cf6cd1e046bbe02b41a21477a4801547","bytes":324556,"gebaut":"2026-09-05T10:46:04Z"}
|
||||
{"version":"2026.9.5.23","sha256":"3ff0de25e28b5cf00a9015ec3af3e27f0e4d0ae8a13eb9e443dd47100ebc9d1e","bytes":324637,"gebaut":"2026-09-05T11:29:36Z"}
|
||||
Binary file not shown.
@@ -3743,6 +3743,18 @@ function vEinst() {
|
||||
if (laeuft === "installieren") return spinner("Installiere …");
|
||||
if (laeuft === "neustart") return spinner("Home Assistant startet neu …");
|
||||
if (u && u.installiert) {
|
||||
/* Ob ein Neustart noetig ist, entscheidet der Updater am
|
||||
Datei-Vergleich (aktualisierung.neustart_noetig). Betrifft das
|
||||
Update nur frontend/, reicht ein Neuladen der Seite: die Dateien
|
||||
werden direkt von der Platte ausgeliefert, und zwar mit
|
||||
cache_headers=False - der Browser holt sie beim naechsten Laden
|
||||
ohnehin frisch. Nur geaenderter Python-Code braucht wirklich einen
|
||||
Neustart (Abschnitt CZ in AGENTS.md). */
|
||||
if (u.neustart_noetig === false) {
|
||||
return `<span style="display:block;margin-top:8px;color:var(--fg2);font-size:13px;line-height:1.6">
|
||||
Version ${esc(u.version)} installiert - nur Oberflächendateien, kein Neustart nötig.</span>
|
||||
<button class="aktion primaer" style="margin-top:14px" data-update="neuladen">Seite neu laden</button>`;
|
||||
}
|
||||
return `<span style="display:block;margin-top:8px;color:var(--fg2);font-size:13px;line-height:1.6">
|
||||
Version ${esc(u.version)} installiert - die neue Fassung wird erst nach einem Neustart
|
||||
geladen.</span>
|
||||
@@ -6693,6 +6705,8 @@ function ereignisseVerdrahten() {
|
||||
);
|
||||
})
|
||||
.finally(() => { INTEGRATION_UPDATE_LAEUFT = null; render(); scrollWiederherstellen(); });
|
||||
} else if (aktion === "neuladen") {
|
||||
location.reload();
|
||||
} else if (aktion === "neustart") {
|
||||
INTEGRATION_UPDATE_LAEUFT = "neustart"; render();
|
||||
HASS.callService("homeassistant", "restart", {}).catch((err) => {
|
||||
|
||||
@@ -48,6 +48,7 @@ from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.helpers.storage import Store
|
||||
|
||||
from . import (
|
||||
aktualisierung,
|
||||
batterie,
|
||||
belege,
|
||||
bilder,
|
||||
@@ -681,6 +682,19 @@ class Koordinator:
|
||||
return None
|
||||
return {**info, "url": BUENDEL_URL}
|
||||
|
||||
async def version_neu_lesen(self) -> None:
|
||||
"""Liest die Versionsnummer erneut von der Platte.
|
||||
|
||||
Nach einem Update OHNE Neustart hat sich die manifest.json geaendert,
|
||||
Home Assistants zwischengespeicherte Fassung aber nicht. Ohne diesen
|
||||
Schritt zeigten Panel und App nach einem reinen Oberflaechen-Update
|
||||
weiter die alte Nummer - und der Nutzer haette keinen Anhalt, ob das
|
||||
Update wirklich angekommen ist."""
|
||||
neu = await self.hass.async_add_executor_job(aktualisierung.version_von_platte)
|
||||
if neu and neu != self.version:
|
||||
_LOGGER.info("Version nach Update: %s -> %s", self.version, neu)
|
||||
self.version = neu
|
||||
|
||||
async def app_version_veroeffentlichen(self) -> None:
|
||||
buendel = await self.hass.async_add_executor_job(self._buendel_lesen)
|
||||
bildstaende = await bilder.staende(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "audi_dashboard",
|
||||
"name": "Audi Dashboard",
|
||||
"version": "2026.9.5.22",
|
||||
"version": "2026.9.5.23",
|
||||
"documentation": "https://gitea.nothaft.cloud/paul/audi-app/src/branch/main/README.md",
|
||||
"issue_tracker": "https://gitea.nothaft.cloud/paul/audi-app/issues",
|
||||
"codeowners": [
|
||||
|
||||
@@ -69,10 +69,12 @@ class ErfolgreicherTausch(unittest.TestCase):
|
||||
"neue_datei.py": b"# neue Fassung\n",
|
||||
"frontend/app.js": b"// panel\n",
|
||||
})
|
||||
version = a.entpacken_pruefen_tauschen(
|
||||
ergebnis = a.entpacken_pruefen_tauschen(
|
||||
zip_bytes, self.integration, self.staging, self.backup
|
||||
)
|
||||
self.assertEqual(version, "2099.1.1.1")
|
||||
self.assertEqual(ergebnis["version"], "2099.1.1.1")
|
||||
# Das Testarchiv enthaelt .py-Dateien, also neustartpflichtig.
|
||||
self.assertTrue(ergebnis["neustart_noetig"])
|
||||
|
||||
# Live-Ordner trägt jetzt die neue Fassung.
|
||||
with open(os.path.join(self.integration, "manifest.json"), "rb") as f:
|
||||
@@ -188,6 +190,73 @@ class AbgelehnteFaelle(unittest.TestCase):
|
||||
self._unangetastet()
|
||||
|
||||
|
||||
class NeustartNoetig(unittest.TestCase):
|
||||
"""Die Unterscheidung "braucht das einen Neustart?" ist bewusst schief.
|
||||
|
||||
Sagt sie faelschlich "nein", laeuft neuer Python-Code nie an und der Fehler
|
||||
wird woanders gesucht. Ein ueberfluessiger Neustart kostet eine Minute.
|
||||
Deshalb: nur frontend/ und manifest.json duerfen ohne Neustart wechseln.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.basis = tempfile.mkdtemp()
|
||||
self.alt = os.path.join(self.basis, "alt")
|
||||
self.neu = os.path.join(self.basis, "neu")
|
||||
for ordner in (self.alt, self.neu):
|
||||
os.makedirs(os.path.join(ordner, "frontend"))
|
||||
self._schreiben(ordner, "manifest.json", '{"version": "1"}')
|
||||
self._schreiben(ordner, "__init__.py", "x = 1\n")
|
||||
self._schreiben(ordner, "frontend/app.js", "alert(1)\n")
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.basis, ignore_errors=True)
|
||||
|
||||
def _schreiben(self, ordner, rel, inhalt):
|
||||
pfad = os.path.join(ordner, *rel.split("/"))
|
||||
os.makedirs(os.path.dirname(pfad), exist_ok=True)
|
||||
with open(pfad, "w", encoding="utf-8") as datei:
|
||||
datei.write(inhalt)
|
||||
|
||||
def test_nichts_geaendert_braucht_keinen_neustart(self):
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_nur_frontend_braucht_keinen_neustart(self):
|
||||
self._schreiben(self.neu, "frontend/app.js", "alert(2)\n")
|
||||
self._schreiben(self.neu, "frontend/neu.css", "body{}\n")
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_manifest_allein_braucht_keinen_neustart(self):
|
||||
# Die Versionsnummer aendert sich bei JEDER Veroeffentlichung - ohne
|
||||
# diese Ausnahme waere jedes Update ein Neustart-Update.
|
||||
self._schreiben(self.neu, "manifest.json", '{"version": "2"}')
|
||||
self._schreiben(self.neu, "frontend/app.js", "alert(2)\n")
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_geaenderte_python_datei_braucht_neustart(self):
|
||||
self._schreiben(self.neu, "__init__.py", "x = 2\n")
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_neue_python_datei_braucht_neustart(self):
|
||||
self._schreiben(self.neu, "neu.py", "y = 1\n")
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_entfallene_python_datei_braucht_neustart(self):
|
||||
os.remove(os.path.join(self.neu, "__init__.py"))
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_services_yaml_braucht_neustart(self):
|
||||
# Nicht weil es zwingend waere, sondern weil die Regel pessimistisch ist.
|
||||
self._schreiben(self.neu, "services.yaml", "dienst:\n")
|
||||
self.assertTrue(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_pycache_zaehlt_nicht(self):
|
||||
self._schreiben(self.neu, "__pycache__/x.cpython-312.pyc", "egal")
|
||||
self.assertFalse(a.neustart_noetig(self.alt, self.neu))
|
||||
|
||||
def test_unlesbarer_ordner_gilt_als_neustart(self):
|
||||
self.assertTrue(a.neustart_noetig(os.path.join(self.basis, "gibtsnicht"), self.neu))
|
||||
|
||||
|
||||
class VersionsOrdnung(unittest.TestCase):
|
||||
"""Nur eine NACHWEISLICH neuere Fassung ist ein Update.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user