Initial code commit
CI / test (3.11) (push) Canceled after 0s
CI / test (3.12) (push) Canceled after 0s
CI / test (3.13) (push) Canceled after 0s
CI / test (3.14) (push) Canceled after 0s
CI / package (push) Canceled after 0s
CI / precommit-and-security (push) Canceled after 11s
CI / typecheck (push) Canceled after 11s

This commit is contained in:
2026-09-25 11:16:19 +01:00
parent 427daa7f1a
commit aded37d491
64 changed files with 8201 additions and 24 deletions
View File
@@ -0,0 +1,36 @@
from __future__ import annotations
import io
import pytest
from schedls.errors import InvalidScheduleError
from schedls.operations.calendar import run_calendar
from schedls.output import Output
from schedls.runner import CommandRunner
pytestmark = pytest.mark.integration
def _has_analyze() -> bool:
return CommandRunner().has("systemd-analyze")
def test_calendar_valid() -> None:
if not _has_analyze():
pytest.skip("systemd-analyze not available")
stream = io.StringIO()
output = Output(color="never", stdout=stream)
run_calendar(CommandRunner(), output, "Mon..Fri 02:30", next_count=3)
text = stream.getvalue()
assert "Normalized" in text
assert "Mon..Fri *-*-* 02:30:00" in text
def test_calendar_invalid() -> None:
if not _has_analyze():
pytest.skip("systemd-analyze not available")
stream = io.StringIO()
output = Output(color="never", stdout=stream)
with pytest.raises(InvalidScheduleError):
run_calendar(CommandRunner(), output, "definitely not a calendar", next_count=1)
+64
View File
@@ -0,0 +1,64 @@
from __future__ import annotations
import os
import pytest
from schedls.backends.cron import CronBackend
from schedls.models import Backend, Command, JobSpec, Scope
from schedls.runner import CommandRunner
pytestmark = pytest.mark.integration
NAME = "schedls-it-cron"
def _backend_or_skip() -> CronBackend:
backend = CronBackend(CommandRunner())
if not backend.available():
pytest.skip("crontab not available")
if os.environ.get("SCHEDLS_RUN_CRON_TESTS") != "1":
pytest.skip("set SCHEDLS_RUN_CRON_TESTS=1 to allow crontab mutation")
return backend
def _restore(backend: CronBackend, original: str) -> None:
backend.runner.run(["crontab", "-"], env_policy="identity", input_text=original, check=False)
def test_cron_lifecycle_preserves_content() -> None:
backend = _backend_or_skip()
original_document = backend.read()
original = original_document.text
# Add an unrelated line first so we can prove preservation.
seeded = original + ("\n" if original and not original.endswith("\n") else "")
seeded += "# unrelated comment\n"
backend.runner.run(["crontab", "-"], env_policy="identity", input_text=seeded)
try:
spec = JobSpec(
name=NAME,
backend=Backend.CRON,
scope=Scope.USER,
command=Command(argv=("/usr/bin/true", "a b", "50%")),
cron_expression="15 2 * * *",
)
plan = backend.plan_create(spec)
result = backend.apply(plan)
assert result.changed
jobs = [job for job in backend.discover([Scope.USER]) if job.name == NAME]
assert len(jobs) == 1
assert jobs[0].managed is True
assert jobs[0].schedule.expression == "15 2 * * *"
raw = backend.read().text
assert "# unrelated comment" in raw
assert "a\\%b" not in raw # sanity: percent is escaped, not raw in a bad way
job = backend.find(NAME)
assert job is not None
backend.apply(backend.plan_remove(job))
assert "# unrelated comment" in backend.read().text
assert backend.find(NAME) is None
finally:
_restore(backend, original)
@@ -0,0 +1,81 @@
from __future__ import annotations
import os
import pytest
from schedls.backends.systemd import SystemdBackend
from schedls.models import Backend, Command, JobSpec, Scope
from schedls.runner import CommandRunner
pytestmark = pytest.mark.integration
NAME = "schedls-it"
def _backend_or_skip() -> SystemdBackend:
backend = SystemdBackend(CommandRunner())
if not backend.has_systemctl():
pytest.skip("systemctl not available")
if not backend.manager_ok(Scope.USER):
pytest.skip("user systemd manager not reachable")
if not backend.has_analyze():
pytest.skip("systemd-analyze not available")
return backend
def _spec(calendar: str) -> JobSpec:
return JobSpec(
name=NAME,
backend=Backend.SYSTEMD,
scope=Scope.USER,
command=Command(argv=("/bin/true",)),
calendar=(calendar,),
persistent=True,
)
def test_systemd_timer_lifecycle() -> None:
backend = _backend_or_skip()
directory = backend._unit_dir(Scope.USER)
timer_path = os.path.join(directory, f"schedls-{NAME}.timer")
service_path = os.path.join(directory, f"schedls-{NAME}.service")
# Clean any leftover from a previous run.
if os.path.exists(timer_path):
existing = backend.find(NAME)
if existing is not None:
backend.apply(backend.plan_remove(existing))
try:
plan = backend.plan_create(_spec("*-*-* 03:30:00"))
result = backend.apply(plan)
assert result.changed
assert os.path.exists(timer_path)
assert os.path.exists(service_path)
job = backend.find(NAME)
assert job is not None
assert job.managed is True
assert job.schedule.expression == "*-*-* 03:30:00"
assert job.command.argv == ("/bin/true",)
backend.apply(backend.plan_set_enabled(job, False))
job = backend.find(NAME)
assert job is not None and job.enabled is False
backend.apply(backend.plan_set_enabled(job, True))
job = backend.find(NAME)
assert job is not None and job.enabled is True
backend.apply(backend.plan_update(job, _spec("*-*-* 05:15:00")))
job = backend.find(NAME)
assert job is not None
assert job.schedule.expression == "*-*-* 05:15:00"
backend.apply(backend.plan_remove(job))
assert not os.path.exists(timer_path)
assert not os.path.exists(service_path)
assert backend.find(NAME) is None
finally:
leftover = backend.find(NAME)
if leftover is not None:
backend.apply(backend.plan_remove(leftover))