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
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:
@@ -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)
|
||||
@@ -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))
|
||||
Reference in New Issue
Block a user