b4fbd6fe9f
Deploy Site / deploy-vercel (push) Has been skipped
Deploy Site / deploy-docs (push) Has been skipped
Build Skills Index / build-index (push) Has been skipped
CI / Deny unrelated histories (push) Has been skipped
CI / Detect affected areas (push) Successful in 27m35s
CI / OSV scan (push) Failing after 4s
CI / Build&Test Docker image (push) Successful in 9s
CI / Supply-chain scan (push) Has been skipped
CI / Lint Docker scripts (push) Failing after 5m13s
CI / Check contributors (push) Failing after 12m8s
CI / Docs Site (push) Failing after 12m8s
CI / TypeScript (push) Failing after 12m8s
CI / Python lints (push) Failing after 12m9s
CI / Python tests (push) Failing after 12m9s
CI / Check uv.lock (push) Failing after 23m22s
CI / CI timing report (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
CI / All required checks pass (push) Has been cancelled
248 lines
7.6 KiB
Python
248 lines
7.6 KiB
Python
"""Tests for systemd optional-directive normalization (issue #41119).
|
|
|
|
On older systemd versions that don't support RestartMaxDelaySec /
|
|
RestartSteps, the installed unit file has those directives silently
|
|
dropped. Without normalization, systemd_unit_is_current() would
|
|
perpetually report the unit as outdated because the strict text
|
|
comparison sees a difference.
|
|
|
|
The fix: _strip_optional_systemd_directives() removes those directives
|
|
from both the installed and expected text before comparison.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _strip_optional_systemd_directives
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestStripOptionalSystemdDirectives:
|
|
def test_removes_restart_max_delay_sec(self):
|
|
from hermes_cli.gateway import _strip_optional_systemd_directives
|
|
text = """[Service]
|
|
Restart=always
|
|
RestartSec=5
|
|
RestartMaxDelaySec=300
|
|
RestartSteps=5
|
|
"""
|
|
result = _strip_optional_systemd_directives(text)
|
|
assert "RestartMaxDelaySec" not in result
|
|
assert "RestartSteps" not in result
|
|
assert "Restart=always" in result
|
|
assert "RestartSec=5" in result
|
|
|
|
def test_preserves_other_directives(self):
|
|
from hermes_cli.gateway import _strip_optional_systemd_directives
|
|
text = """[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python gateway run
|
|
Restart=always
|
|
RestartSec=5
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
"""
|
|
result = _strip_optional_systemd_directives(text)
|
|
assert "Type=simple" in result
|
|
assert "ExecStart=" in result
|
|
assert "KillMode=mixed" in result
|
|
assert "KillSignal=SIGTERM" in result
|
|
|
|
def test_handles_empty_string(self):
|
|
from hermes_cli.gateway import _strip_optional_systemd_directives
|
|
assert _strip_optional_systemd_directives("") == ""
|
|
|
|
def test_handles_no_optional_directives(self):
|
|
from hermes_cli.gateway import _strip_optional_systemd_directives
|
|
text = "[Service]\nRestart=always\n"
|
|
result = _strip_optional_systemd_directives(text)
|
|
assert "Restart=always" in result
|
|
assert "RestartMaxDelaySec" not in result
|
|
|
|
def test_preserves_comments(self):
|
|
from hermes_cli.gateway import _strip_optional_systemd_directives
|
|
text = """[Service]
|
|
# RestartMaxDelaySec is set below
|
|
RestartMaxDelaySec=300
|
|
"""
|
|
result = _strip_optional_systemd_directives(text)
|
|
# The comment line should be preserved
|
|
assert "# RestartMaxDelaySec" in result
|
|
# The actual directive should be removed
|
|
assert "RestartMaxDelaySec=300" not in result
|
|
|
|
def test_handles_inline_values_with_equals(self):
|
|
from hermes_cli.gateway import _strip_optional_systemd_directives
|
|
text = "RestartMaxDelaySec=300\n"
|
|
result = _strip_optional_systemd_directives(text)
|
|
assert result == ""
|
|
|
|
def test_full_unit_comparison(self):
|
|
"""Simulate the full stale-check flow with an older systemd unit."""
|
|
from hermes_cli.gateway import (
|
|
_normalize_service_definition,
|
|
_strip_optional_systemd_directives,
|
|
)
|
|
# What the installed unit looks like on older systemd (directives stripped)
|
|
installed = """[Unit]
|
|
Description=Hermes Gateway
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python -m hermes_cli.main gateway run
|
|
Restart=always
|
|
RestartSec=5
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
# What generate_systemd_unit produces (with the directives)
|
|
expected = """[Unit]
|
|
Description=Hermes Gateway
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python -m hermes_cli.main gateway run
|
|
Restart=always
|
|
RestartSec=5
|
|
RestartMaxDelaySec=300
|
|
RestartSteps=5
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
# Without normalization, they differ
|
|
assert _normalize_service_definition(installed) != _normalize_service_definition(expected)
|
|
|
|
# With optional-directive stripping, they match
|
|
norm_installed = _normalize_service_definition(
|
|
_strip_optional_systemd_directives(installed)
|
|
)
|
|
norm_expected = _normalize_service_definition(
|
|
_strip_optional_systemd_directives(expected)
|
|
)
|
|
assert norm_installed == norm_expected
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# systemd_unit_is_current integration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSystemdUnitIsCurrent:
|
|
def test_unit_without_optional_directives_is_current(self, tmp_path, monkeypatch):
|
|
"""Installed unit missing RestartMaxDelaySec/RestartSteps should be
|
|
considered current when the generated unit includes them."""
|
|
from hermes_cli import gateway as gw
|
|
|
|
installed = """[Unit]
|
|
Description=Hermes Gateway
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python gateway run
|
|
Restart=always
|
|
RestartSec=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
unit_file = tmp_path / "hermes-gateway.service"
|
|
unit_file.write_text(installed)
|
|
|
|
monkeypatch.setattr(gw, "get_systemd_unit_path", lambda system=False: unit_file)
|
|
monkeypatch.setattr(
|
|
gw,
|
|
"generate_systemd_unit",
|
|
lambda system=False, run_as_user=None: installed + "\nRestartMaxDelaySec=300\nRestartSteps=5\n",
|
|
)
|
|
|
|
assert gw.systemd_unit_is_current(system=False) is True
|
|
|
|
def test_unit_with_different_restart_is_not_current(self, tmp_path, monkeypatch):
|
|
"""A unit with genuinely different config should still be outdated."""
|
|
from hermes_cli import gateway as gw
|
|
|
|
installed = """[Unit]
|
|
Description=Hermes Gateway
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python gateway run
|
|
Restart=always
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
expected = """[Unit]
|
|
Description=Hermes Gateway
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python gateway run
|
|
Restart=always
|
|
RestartSec=5
|
|
RestartMaxDelaySec=300
|
|
RestartSteps=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
unit_file = tmp_path / "hermes-gateway.service"
|
|
unit_file.write_text(installed)
|
|
|
|
monkeypatch.setattr(gw, "get_systemd_unit_path", lambda system=False: unit_file)
|
|
monkeypatch.setattr(
|
|
gw,
|
|
"generate_systemd_unit",
|
|
lambda system=False, run_as_user=None: expected,
|
|
)
|
|
|
|
assert gw.systemd_unit_is_current(system=False) is False
|
|
|
|
def test_unit_with_optional_directives_is_current(self, tmp_path, monkeypatch):
|
|
"""Installed unit WITH the optional directives should also be current."""
|
|
from hermes_cli import gateway as gw
|
|
|
|
unit_text = """[Unit]
|
|
Description=Hermes Gateway
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=/usr/bin/python gateway run
|
|
Restart=always
|
|
RestartSec=5
|
|
RestartMaxDelaySec=300
|
|
RestartSteps=5
|
|
|
|
[Install]
|
|
WantedBy=default.target
|
|
"""
|
|
unit_file = tmp_path / "hermes-gateway.service"
|
|
unit_file.write_text(unit_text)
|
|
|
|
monkeypatch.setattr(gw, "get_systemd_unit_path", lambda system=False: unit_file)
|
|
monkeypatch.setattr(
|
|
gw,
|
|
"generate_systemd_unit",
|
|
lambda system=False, run_as_user=None: unit_text,
|
|
)
|
|
|
|
assert gw.systemd_unit_is_current(system=False) is True
|
|
|
|
def test_nonexistent_unit_is_not_current(self, tmp_path, monkeypatch):
|
|
from hermes_cli import gateway as gw
|
|
unit_file = tmp_path / "nonexistent.service"
|
|
monkeypatch.setattr(gw, "get_systemd_unit_path", lambda system=False: unit_file)
|
|
assert gw.systemd_unit_is_current(system=False) is False
|