Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7006991843 | ||
|
|
8a4a01c42f |
No files matched your search
@@ -1,3 +1,11 @@
|
|||||||
|
# 2026-10-21 — Erfolgreiche Hermes-Prüfung ohne Versionswechsel
|
||||||
|
|
||||||
|
- Ein unveränderter Commit nach erfolgreichem `hermes update` gilt als Erfolg:
|
||||||
|
Der konfigurierte Stable-Kanal kann aktuell sein, obwohl `origin/main` weiter ist.
|
||||||
|
- Abschließende Meldungen erscheinen direkt im Journal des Update-Dienstes.
|
||||||
|
- Isolierte Regressionstests prüfen unveränderte/aktualisierte Commits,
|
||||||
|
Updater-Fehler und Gateway-Rollback ohne Netzwerk oder echte Dienstaufrufe.
|
||||||
|
|
||||||
# 2026-10-12 — VRAM-Reserve und portable CPU-Kompression
|
# 2026-10-12 — VRAM-Reserve und portable CPU-Kompression
|
||||||
|
|
||||||
- `desktop-64k` mit 4096 MiB Fit-Zielmarge und 64K Kontext als Standard neuer
|
- `desktop-64k` mit 4096 MiB Fit-Zielmarge und 64K Kontext als Standard neuer
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Reproduzierbares Benutzer-Deployment für Hermes Agent auf Arch Linux mit:
|
|||||||
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench
|
- optionalem CUDA-Build von llama-server, llama-cli und llama-bench
|
||||||
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
|
- Qwen-/RTX-3080-Profilen mit prüfbarem Wechsel und Rückweg
|
||||||
|
|
||||||
Stand: 2026-10-12. Für deine **bestehende Workstation** zuerst
|
Stand: 2026-10-21. Für deine **bestehende Workstation** zuerst
|
||||||
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
|
[`docs/WORKSTATION.md`](docs/WORKSTATION.md) lesen. Der Schnellstart unten ist für
|
||||||
eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
|
eine neue Maschine. Hardware-Leistungsgewinne sind noch nicht auf deiner GPU
|
||||||
gemessen; die kurze Funktions- und VRAM-Abnahme vom 12.10.2026 ist in
|
gemessen; die kurze Funktions- und VRAM-Abnahme vom 12.10.2026 ist in
|
||||||
@@ -150,7 +150,7 @@ Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr au
|
|||||||
|
|
||||||
1. exklusives Lock und Prüfung auf lokale Änderungen
|
1. exklusives Lock und Prüfung auf lokale Änderungen
|
||||||
2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback
|
2. `origin/main`-Fetch mit begrenzten Retries und letztem HTTP/1.1-Fallback
|
||||||
3. schneller Abschluss ohne Backup, wenn kein Update vorliegt
|
3. schneller Abschluss ohne Backup, wenn Checkout und gefetchter Branch identisch sind
|
||||||
4. komprimiertes Backup vor jeder Änderung
|
4. komprimiertes Backup vor jeder Änderung
|
||||||
5. Stoppen des Gateways und Aufruf von `hermes update`
|
5. Stoppen des Gateways und Aufruf von `hermes update`
|
||||||
6. einmaliger Retry bei eindeutig transientem Fetch-Fehler
|
6. einmaliger Retry bei eindeutig transientem Fetch-Fehler
|
||||||
@@ -159,9 +159,17 @@ Update-Lock und löst bei Backend-HTTP-Timeouts keine Inferenz-Neustarts mehr au
|
|||||||
9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback
|
9. separater llama.cpp-Healthcheck ohne unsinnigen Hermes-Rollback
|
||||||
10. Aufbewahrung der drei neuesten Hermes-Backups
|
10. Aufbewahrung der drei neuesten Hermes-Backups
|
||||||
|
|
||||||
|
Den Zielstand wählt `hermes update` anhand des konfigurierten Update-Kanals.
|
||||||
|
Bei Stable kann der Checkout bereits aktuell sein, obwohl `origin/main` weiter
|
||||||
|
ist. Ein unveränderter Commit bei Updater-Exitcode 0 und gesundem Gateway gilt
|
||||||
|
deshalb als Erfolg. Dabei können Backup und Gateway-Neustart trotzdem stattfinden.
|
||||||
|
Ein fehlgeschlagener Updater bei unverändertem Commit bleibt ein Fehler.
|
||||||
|
|
||||||
Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen
|
Ein Fehler von Telegram oder Signal kann den Updateprozess niemals fehlschlagen
|
||||||
lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste
|
lassen. Detaillierte Zwischenstände landen im Journal; nur zusammengefasste
|
||||||
Ergebnisse werden versendet.
|
Ergebnisse werden versendet. Abschlussmeldungen stehen auch direkt im Journal
|
||||||
|
von `hermes-stack-hermes-update.service`. Diagnose, gezielte Installation und
|
||||||
|
Prüfung sind in [HERMES_UPDATE.md](docs/HERMES_UPDATE.md) beschrieben.
|
||||||
|
|
||||||
## Bedienung
|
## Bedienung
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Hermes-Update betreiben und prüfen
|
||||||
|
|
||||||
|
## Bereits aktuelle Stable-Version
|
||||||
|
|
||||||
|
Der Updater prüft zunächst den konfigurierten Remote-Branch (standardmäßig
|
||||||
|
`origin/main`). Den tatsächlichen Zielstand wählt anschließend `hermes update`
|
||||||
|
anhand des Hermes-Update-Kanals. Bei Stable kann der installierte Release-Commit
|
||||||
|
aktuell sein, obwohl auf `main` neuere Entwicklungscommits liegen.
|
||||||
|
|
||||||
|
Bis zur Korrektur vom 21.10.2026 endete dieser Fall trotz `Already up to date!`
|
||||||
|
und Updater-Exitcode 0 mit einem fehlgeschlagenen Wartungsdienst. Das Skript
|
||||||
|
forderte fälschlich einen Commitwechsel. Jetzt endet ein erfolgreicher Updater
|
||||||
|
mit unverändertem Commit nach den Konfigurations- und Gateway-Prüfungen
|
||||||
|
erfolgreich. Der Kanal wird durch diese Korrektur nicht geändert.
|
||||||
|
|
||||||
|
Ein fehlgeschlagener Updater bei unverändertem Commit liefert weiterhin Exitcode
|
||||||
|
1. Ein ungesunder Gateway löst weiterhin den vorhandenen Rollback aus. Der
|
||||||
|
separate llama-Endpunkt wird unabhängig geprüft; dessen Ausfall führt nicht zu
|
||||||
|
einem Hermes-Rollback.
|
||||||
|
|
||||||
|
## Nur den korrigierten Wrapper installieren
|
||||||
|
|
||||||
|
Im Repository-Verzeichnis ausführen, wenn gerade kein Update läuft. Vorher
|
||||||
|
laufende Hermes-Aufgaben beenden lassen. Diese gezielte Installation ersetzt nur
|
||||||
|
den Wrapper; `./install.sh` installiert dagegen alle versionierten Wrapper/Units.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
backup_dir="$(mktemp -d "$HOME/.local/state/hermes-update-wrapper.XXXXXXXX")"
|
||||||
|
cp -p ~/.local/libexec/hermes-stack-hermes-update "$backup_dir/"
|
||||||
|
printf 'Wrapper-Sicherung: %s\n' "$backup_dir"
|
||||||
|
install -m 0755 libexec/hermes-stack-hermes-update \
|
||||||
|
~/.local/libexec/hermes-stack-hermes-update
|
||||||
|
cmp libexec/hermes-stack-hermes-update \
|
||||||
|
~/.local/libexec/hermes-stack-hermes-update
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Unit bleibt unverändert; dafür ist kein `daemon-reload` erforderlich.
|
||||||
|
Die vorherige Wrapper-Version liegt im ausgegebenen bzw. in `$backup_dir`
|
||||||
|
gespeicherten Sicherungsverzeichnis. Bei Bedarf dieselbe Datei mit `install
|
||||||
|
-m 0755` zurückkopieren, während kein Update läuft.
|
||||||
|
|
||||||
|
## Manueller Lauf und Diagnose
|
||||||
|
|
||||||
|
Der Lauf kann den Gateway kurz stoppen und neu starten. Er erzeugt nach dem
|
||||||
|
Remote-Vergleich gegebenenfalls auch dann ein Backup, wenn Stable bereits aktuell
|
||||||
|
ist. Standardmäßig werden die drei neuesten Hermes-Backups behalten.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user start hermes-stack-hermes-update.service
|
||||||
|
systemctl --user show hermes-stack-hermes-update.service \
|
||||||
|
-p ActiveState -p SubState -p Result -p ExecMainStatus
|
||||||
|
systemctl --user is-active hermes-gateway.service
|
||||||
|
journalctl --user -u hermes-stack-hermes-update.service -n 100 --no-pager
|
||||||
|
journalctl --user -t hermes-stack-maintenance -n 100 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `Result=success`, `ExecMainStatus=0` und ein aktiver Gateway. Als
|
||||||
|
`Type=oneshot` ist der Update-Dienst nach erfolgreichem Abschluss selbst wieder
|
||||||
|
`inactive/dead`. Die Meldung `Hermes ist im konfigurierten Update-Kanal bereits
|
||||||
|
aktuell` bestätigt den erfolgreichen Lauf ohne Versionswechsel.
|
||||||
|
|
||||||
|
Updater-Ausgaben liegen zusätzlich unter
|
||||||
|
`~/.local/state/hermes-stack-maintenance/hermes-update-*.log`.
|
||||||
|
Wrapper-Abschlussmeldungen stehen im Dienstjournal; die Benachrichtigung schreibt
|
||||||
|
zusätzlich unter dem Tag `hermes-stack-maintenance`.
|
||||||
|
|
||||||
|
Die Abnahme auf Archwork und ihre Prüfgrenzen stehen in
|
||||||
|
[VALIDATION.md](VALIDATION.md).
|
||||||
@@ -1,3 +1,20 @@
|
|||||||
|
# Validierung vom 2026-10-21
|
||||||
|
|
||||||
|
- `make test`: Syntax- und Secret-Musterprüfung sowie **31 Funktionstests
|
||||||
|
bestanden** (26 vorhandene Tests und fünf neue Updater-Regressionstests).
|
||||||
|
- Neue isolierte Tests: unveränderter Stable-Commit bei neuerem `main`,
|
||||||
|
fehlgeschlagener Updater ohne Commitwechsel, erfolgreicher Commitwechsel,
|
||||||
|
Commitwechsel mit Updater-Restartwarnung und Gateway-Rollback.
|
||||||
|
Externe Programme werden simuliert; kein echtes Netzwerk oder systemd darin.
|
||||||
|
- `git diff --check`: erfolgreich. ShellCheck ist lokal nicht installiert.
|
||||||
|
- Korrigierter Wrapper auf Archwork installiert und bytegleich geprüft.
|
||||||
|
- Echter Update-Dienstlauf am 21.10.2026, 16:26 MESZ: Stable v0.22.3 blieb auf
|
||||||
|
Commit `5b147ab036d6d19131bff8d20701b5001eab844a`; Abschluss nach rund 47 Sekunden
|
||||||
|
mit `Result=success` und `ExecMainStatus=0`. Gateway danach `active/running`.
|
||||||
|
Die neue Erfolgsmeldung erschien im Journal des Update-Dienstes.
|
||||||
|
- Ein echter Versionswechsel und ein echter Rollback wurden bei dieser Abnahme
|
||||||
|
nicht ausgelöst. Ein vollständiger Installer-Lauf wurde nicht durchgeführt.
|
||||||
|
|
||||||
# Validierung vom 2026-10-12
|
# Validierung vom 2026-10-12
|
||||||
|
|
||||||
- `make test`: Shell-Syntaxprüfung, Secret-Mustercheck und **26 isolierte
|
- `make test`: Shell-Syntaxprüfung, Secret-Mustercheck und **26 isolierte
|
||||||
|
|||||||
@@ -147,3 +147,16 @@ Repo-Dateien wurden danach aufgenommen und getestet, aber nicht erneut über
|
|||||||
`install.sh` auf die Workstation ausgerollt. Der aktuelle Betrieb bleibt bei den
|
`install.sh` auf die Workstation ausgerollt. Der aktuelle Betrieb bleibt bei den
|
||||||
bereits geprüften Einstellungen. Details zur Übertragung und zu bestehenden
|
bereits geprüften Einstellungen. Details zur Übertragung und zu bestehenden
|
||||||
Units/Drop-ins stehen in [RTX3080.md](RTX3080.md).
|
Units/Drop-ins stehen in [RTX3080.md](RTX3080.md).
|
||||||
|
|
||||||
|
## Hermes-Update-Korrektur vom 2026-10-21
|
||||||
|
|
||||||
|
Der korrigierte `hermes-stack-hermes-update` ist gezielt auf der Workstation
|
||||||
|
installiert und mit der Repository-Datei bytegleich geprüft. Der echte
|
||||||
|
Dienstlauf mit bereits aktuellem Stable v0.22.3 endete erfolgreich
|
||||||
|
(`Result=success`, `ExecMainStatus=0`); der Gateway war danach `active/running`.
|
||||||
|
Abschlussmeldungen erscheinen jetzt direkt im Dienstjournal.
|
||||||
|
|
||||||
|
Vorherige Wrapper-Version:
|
||||||
|
`~/.local/libexec/hermes-stack-hermes-update.before-noop-fix-20261021`.
|
||||||
|
Ein vollständiger Installer-Lauf war für diese Korrektur nicht erforderlich.
|
||||||
|
Betrieb und Fehlerdiagnose: [HERMES_UPDATE.md](HERMES_UPDATE.md).
|
||||||
@@ -15,6 +15,8 @@ log() {
|
|||||||
|
|
||||||
notify_user() {
|
notify_user() {
|
||||||
local text="$1"
|
local text="$1"
|
||||||
|
# Keep the outcome in this service's journal as well as the notifier's journal.
|
||||||
|
printf '%s\n' "$text"
|
||||||
if [[ -x "$NOTIFIER" ]]; then
|
if [[ -x "$NOTIFIER" ]]; then
|
||||||
"$NOTIFIER" "$text" || true
|
"$NOTIFIER" "$text" || true
|
||||||
else
|
else
|
||||||
@@ -226,7 +228,12 @@ mapfile -t old_backups < <(
|
|||||||
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
|
((${#old_backups[@]} == 0)) || rm -f -- "${old_backups[@]}"
|
||||||
|
|
||||||
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
|
new_commit="$(git -C "$HERMES_REPO" rev-parse HEAD)"
|
||||||
if [[ "$new_commit" == "$old_commit" ]]; then
|
# The updater selects the configured channel (e.g. stable), whereas the fetch
|
||||||
|
# above only checks remote connectivity. Stable may legitimately stay on the
|
||||||
|
# same commit even when origin/main has advanced.
|
||||||
|
if [[ "$new_commit" == "$old_commit" ]] && ((update_exit == 0)); then
|
||||||
|
notify_user "Hermes ist im konfigurierten Update-Kanal bereits aktuell: $new_commit. Konfiguration und frisch gestarteter Gateway sind gesund."
|
||||||
|
elif [[ "$new_commit" == "$old_commit" ]]; then
|
||||||
notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund."
|
notify_user "Hermes-Update wurde nicht installiert (Updater-Exitcode $update_exit); der bisherige Commit $new_commit und der frisch gestartete Gateway sind gesund."
|
||||||
exit 1
|
exit 1
|
||||||
elif ((update_exit != 0)); then
|
elif ((update_exit != 0)); then
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
|
python3 "$ROOT_DIR/tests/test_llama.py" || failures=$((failures + 1))
|
||||||
|
python3 "$ROOT_DIR/tests/test_hermes_update.py" || failures=$((failures + 1))
|
||||||
|
|
||||||
if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
|
if (cd -- "$ROOT_DIR" && rg -n --hidden --glob '!tests/smoke.sh' --glob '!tests/test_llama.py' --glob '!.git/**' \
|
||||||
'(TELEGRAM_BOT_TOKEN|SIGNAL_ACCOUNT|API_KEY)=("|'"'"')?[A-Za-z0-9+]' .); then
|
'(TELEGRAM_BOT_TOKEN|SIGNAL_ACCOUNT|API_KEY)=("|'"'"')?[A-Za-z0-9+]' .); then
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Run the actual maintenance script with isolated state and fake external tools."""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RUNNER = ROOT / "libexec/hermes-stack-hermes-update"
|
||||||
|
|
||||||
|
|
||||||
|
class HermesUpdateTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.temp = tempfile.TemporaryDirectory(prefix="hermes-update-", dir=ROOT.parent)
|
||||||
|
self.addCleanup(self.temp.cleanup)
|
||||||
|
self.root = Path(self.temp.name)
|
||||||
|
self.bin = self.root / "bin"
|
||||||
|
self.bin.mkdir()
|
||||||
|
config = self.root / "config/hermes-stack-maintenance"
|
||||||
|
config.mkdir(parents=True)
|
||||||
|
(self.root / "hermes-agent/.git").mkdir(parents=True)
|
||||||
|
self.env = dict(os.environ, XDG_CONFIG_HOME=str(config.parent),
|
||||||
|
XDG_STATE_HOME=str(self.root / "state"),
|
||||||
|
XDG_RUNTIME_DIR=str(self.root), TEST_ROOT=str(self.root),
|
||||||
|
PATH=str(self.bin) + ":" + os.environ["PATH"],
|
||||||
|
TEST_UPDATE_EXIT="0", TEST_CHANGED="0", TEST_HEALTHY="1")
|
||||||
|
(config / "config.env").write_text(
|
||||||
|
f'HERMES_REPO={shlex.quote(str(self.root / "hermes-agent"))}\n'
|
||||||
|
f'NOTIFIER={shlex.quote(str(self.bin / "notify-test"))}\n'
|
||||||
|
'HERMES_REMOTE=origin\nHERMES_BRANCH=main\n'
|
||||||
|
'GATEWAY_SERVICE=test-gateway.service\n'
|
||||||
|
'GATEWAY_HEALTH_ATTEMPTS=1\nGATEWAY_HEALTH_DELAY=0\n'
|
||||||
|
'LLAMA_URL=http://unused.invalid\n')
|
||||||
|
self.program("git", '''case "$*" in
|
||||||
|
*"status --porcelain"*|*"fetch --no-tags"*) exit 0 ;;
|
||||||
|
*"rev-parse HEAD"*)
|
||||||
|
if [[ -f "$TEST_ROOT/changed" ]]; then printf 'c%.0s' {1..40};
|
||||||
|
else printf 'a%.0s' {1..40}; fi ;;
|
||||||
|
*"rev-parse refs/remotes/"*) printf 'b%.0s' {1..40} ;;
|
||||||
|
*) exit 99 ;;
|
||||||
|
esac
|
||||||
|
''')
|
||||||
|
self.program("hermes", '''case "$*" in
|
||||||
|
update)
|
||||||
|
[[ "$TEST_CHANGED" == 0 ]] || touch "$TEST_ROOT/changed"
|
||||||
|
echo "Simulated updater completion"
|
||||||
|
exit "$TEST_UPDATE_EXIT" ;;
|
||||||
|
"gateway status") [[ "$TEST_HEALTHY" == 1 ]] ;;
|
||||||
|
"config check"|"config migrate") exit 0 ;;
|
||||||
|
*) exit 99 ;;
|
||||||
|
esac
|
||||||
|
''')
|
||||||
|
self.program("systemctl", 'printf "%s\\n" "$*" >>"$TEST_ROOT/systemctl-calls"\n')
|
||||||
|
self.program("tar", 'printf "%s\\n" "$*" >>"$TEST_ROOT/tar-calls"\n')
|
||||||
|
self.program("curl", "echo '{\"status\":\"ok\"}'\n")
|
||||||
|
self.program("notify-test", 'printf "%s\\n" "$*" >>"$TEST_ROOT/notices"\n')
|
||||||
|
self.program("systemd-cat", "cat >/dev/null\n")
|
||||||
|
self.program("sleep", "exit 0\n")
|
||||||
|
|
||||||
|
def program(self, name, body):
|
||||||
|
path = self.bin / name
|
||||||
|
path.write_text("#!/usr/bin/env bash\nset -eu\n" + body)
|
||||||
|
path.chmod(0o755)
|
||||||
|
|
||||||
|
def run_update(self, **settings):
|
||||||
|
return subprocess.run(["bash", str(RUNNER)], env=dict(self.env, **settings),
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
|
||||||
|
def test_stable_unchanged_with_newer_main_succeeds(self):
|
||||||
|
result = self.run_update()
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn("bereits aktuell", result.stdout)
|
||||||
|
self.assertIn("restart test-gateway.service", (self.root / "systemctl-calls").read_text())
|
||||||
|
self.assertNotIn("zurückgerollt", (self.root / "notices").read_text())
|
||||||
|
|
||||||
|
def test_unchanged_failed_updater_still_fails(self):
|
||||||
|
result = self.run_update(TEST_UPDATE_EXIT="7")
|
||||||
|
self.assertEqual(result.returncode, 1, result.stderr)
|
||||||
|
self.assertIn("Updater-Exitcode 7", result.stdout)
|
||||||
|
|
||||||
|
def test_changed_commit_succeeds(self):
|
||||||
|
result = self.run_update(TEST_CHANGED="1")
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn("erfolgreich aktualisiert", result.stdout)
|
||||||
|
|
||||||
|
def test_changed_commit_with_restart_warning(self):
|
||||||
|
result = self.run_update(TEST_CHANGED="1", TEST_UPDATE_EXIT="7")
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn("Updater-Restartwarnung", result.stdout)
|
||||||
|
|
||||||
|
def test_unhealthy_gateway_still_rolls_back(self):
|
||||||
|
result = self.run_update(TEST_HEALTHY="0")
|
||||||
|
self.assertEqual(result.returncode, 1, result.stderr)
|
||||||
|
self.assertIn("zurückgerollt", result.stdout)
|
||||||
|
self.assertIn("-xf", (self.root / "tar-calls").read_text())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user