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,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
|
||||
import pytest
|
||||
|
||||
from schedls.cli import _spec_for_edit, _spec_for_new, _wizard_edit, _wizard_new, build_parser
|
||||
from schedls.errors import ConfirmationRequiredError, InvalidScheduleError
|
||||
from schedls.models import (
|
||||
Backend,
|
||||
Command,
|
||||
JobSource,
|
||||
Schedule,
|
||||
ScheduledJob,
|
||||
ScheduleKind,
|
||||
Scope,
|
||||
SystemdDetails,
|
||||
)
|
||||
from schedls.output import Output
|
||||
from schedls.prompt import Prompter
|
||||
|
||||
|
||||
class FakeInput:
|
||||
def __init__(self, answers: list[str]) -> None:
|
||||
self.answers = list(answers)
|
||||
self.prompts: list[str] = []
|
||||
|
||||
def __call__(self, label: str) -> str:
|
||||
self.prompts.append(label)
|
||||
if not self.answers:
|
||||
raise EOFError
|
||||
return self.answers.pop(0)
|
||||
|
||||
|
||||
class FakeStdin:
|
||||
def __init__(self, *, isatty: bool) -> None:
|
||||
self._isatty = isatty
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return self._isatty
|
||||
|
||||
|
||||
def make_prompter(answers: list[str], *, calendar_validator=None) -> tuple[Prompter, FakeInput]:
|
||||
fake = FakeInput(answers)
|
||||
output = Output(stdout=io.StringIO(), stderr=io.StringIO())
|
||||
return Prompter(output=output, input_fn=fake, calendar_validator=calendar_validator), fake
|
||||
|
||||
|
||||
def _systemd_job() -> ScheduledJob:
|
||||
return ScheduledJob(
|
||||
name="backup",
|
||||
backend=Backend.SYSTEMD,
|
||||
scope=Scope.USER,
|
||||
managed=True,
|
||||
enabled=True,
|
||||
schedule=Schedule(ScheduleKind.CALENDAR, "*-*-* 02:00:00"),
|
||||
command=Command(argv=("/usr/local/bin/backup",)),
|
||||
source=JobSource(detail="systemd user timer"),
|
||||
systemd=SystemdDetails(on_calendar=("*-*-* 02:00:00",)),
|
||||
)
|
||||
|
||||
|
||||
def test_text_requires_a_value() -> None:
|
||||
prompter, fake = make_prompter(["", "backup"])
|
||||
assert prompter.text("Name") == "backup"
|
||||
assert len(fake.prompts) == 2
|
||||
|
||||
|
||||
def test_text_uses_default_on_blank() -> None:
|
||||
prompter, _ = make_prompter([""])
|
||||
assert prompter.text("Name", default="backup") == "backup"
|
||||
|
||||
|
||||
def test_optional_text() -> None:
|
||||
prompter, _ = make_prompter(["", ""])
|
||||
assert prompter.optional_text("Jitter") is None
|
||||
assert prompter.optional_text("Jitter", default="5min") == "5min"
|
||||
|
||||
|
||||
def test_yes_no_rejects_unknown_answers() -> None:
|
||||
prompter, _ = make_prompter(["maybe", "yes"])
|
||||
assert prompter.yes_no("Continue?") is True
|
||||
|
||||
|
||||
def test_choice_by_number_and_default() -> None:
|
||||
prompter, _ = make_prompter(["9", "2", ""])
|
||||
options = [("daily", "Daily"), ("weekly", "Weekly")]
|
||||
assert prompter.choice("Schedule:", options, default="daily") == "weekly"
|
||||
assert prompter.choice("Schedule:", options, default="daily") == "daily"
|
||||
|
||||
|
||||
def test_command_builds_argv() -> None:
|
||||
prompter, _ = make_prompter(["n", "/usr/bin/echo hello world"])
|
||||
command = prompter.command()
|
||||
assert command.shell is False
|
||||
assert command.argv == ("/usr/bin/echo", "hello", "world")
|
||||
|
||||
|
||||
def test_command_shell_mode() -> None:
|
||||
prompter, _ = make_prompter(["y", "echo hi | cat"])
|
||||
command = prompter.command()
|
||||
assert command.shell is True
|
||||
assert command.raw == "echo hi | cat"
|
||||
|
||||
|
||||
def test_environment_collects_valid_entries() -> None:
|
||||
prompter, _ = make_prompter(["A=1", "1BAD=2", "B=x=y", ""])
|
||||
assert prompter.environment() == ["A=1", "B=x=y"]
|
||||
|
||||
|
||||
def test_schedule_daily_reprompts_on_bad_time() -> None:
|
||||
prompter, _ = make_prompter(["", "25:00", "02:30"])
|
||||
assert prompter.schedule(Backend.SYSTEMD) == {"daily": "02:30"}
|
||||
|
||||
|
||||
def test_schedule_weekly_and_monthly() -> None:
|
||||
prompter, _ = make_prompter(["3", "Mon", "02:00"])
|
||||
assert prompter.schedule(Backend.SYSTEMD) == {"weekly": ["Mon", "02:00"]}
|
||||
prompter, _ = make_prompter(["4", "15", "06:00"])
|
||||
assert prompter.schedule(Backend.SYSTEMD) == {"monthly": ["15", "06:00"]}
|
||||
|
||||
|
||||
def test_schedule_custom_systemd_validates() -> None:
|
||||
def validator(expression: str) -> None:
|
||||
if expression != "daily":
|
||||
raise InvalidScheduleError("bad expression")
|
||||
|
||||
prompter, _ = make_prompter(["5", "bogus", "daily"], calendar_validator=validator)
|
||||
assert prompter.schedule(Backend.SYSTEMD) == {"calendar": ["daily"]}
|
||||
|
||||
|
||||
def test_schedule_custom_cron() -> None:
|
||||
prompter, _ = make_prompter(["5", "0 4 * * *"])
|
||||
assert prompter.schedule(Backend.CRON) == {"cron_expr": "0 4 * * *"}
|
||||
|
||||
|
||||
def test_require_terminal_rejects_non_tty(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("sys.stdin", FakeStdin(isatty=False))
|
||||
prompter, _ = make_prompter([])
|
||||
with pytest.raises(ConfirmationRequiredError):
|
||||
prompter.require_terminal()
|
||||
|
||||
|
||||
def test_eof_raises_confirmation_required() -> None:
|
||||
prompter, _ = make_prompter([])
|
||||
with pytest.raises(ConfirmationRequiredError):
|
||||
prompter.text("Name")
|
||||
|
||||
|
||||
def test_wizard_new_systemd(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("sys.stdin", FakeStdin(isatty=True))
|
||||
args = build_parser().parse_args(["new", "-i"])
|
||||
answers = ["backup", "", "n", "/usr/local/bin/backup /srv/data", "", "02:00", "n"]
|
||||
prompter, _ = make_prompter(answers)
|
||||
args, tail = _wizard_new(args, [], prompter)
|
||||
spec = _spec_for_new(args, tail)
|
||||
assert spec.name == "backup"
|
||||
assert spec.backend is Backend.SYSTEMD
|
||||
assert spec.calendar == ("*-*-* 02:00:00",)
|
||||
assert spec.command.argv == ("/usr/local/bin/backup", "/srv/data")
|
||||
|
||||
|
||||
def test_wizard_new_cron(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("sys.stdin", FakeStdin(isatty=True))
|
||||
args = build_parser().parse_args(["new", "-i"])
|
||||
answers = ["cleanup", "2", "n", "/usr/local/bin/cleanup", "", "04:00"]
|
||||
prompter, _ = make_prompter(answers)
|
||||
args, tail = _wizard_new(args, [], prompter)
|
||||
spec = _spec_for_new(args, tail)
|
||||
assert spec.backend is Backend.CRON
|
||||
assert spec.cron_expression == "0 4 * * *"
|
||||
|
||||
|
||||
def test_wizard_new_skips_provided_fields(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("sys.stdin", FakeStdin(isatty=True))
|
||||
args = build_parser().parse_args(["new", "backup", "-i", "--timer", "--daily", "02:00"])
|
||||
prompter, fake = make_prompter(["n"])
|
||||
args, tail = _wizard_new(args, ["/bin/true"], prompter)
|
||||
spec = _spec_for_new(args, tail)
|
||||
assert spec.calendar == ("*-*-* 02:00:00",)
|
||||
assert fake.prompts == ["Set advanced timer options? [y/N] "]
|
||||
|
||||
|
||||
def test_wizard_edit_changes_schedule(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("sys.stdin", FakeStdin(isatty=True))
|
||||
job = _systemd_job()
|
||||
args = build_parser().parse_args(["edit", "backup", "-i"])
|
||||
answers = ["n", "y", "", "03:00", "n"]
|
||||
prompter, _ = make_prompter(answers)
|
||||
args, tail = _wizard_edit(job, args, [], prompter)
|
||||
spec = _spec_for_edit(job, args, tail)
|
||||
assert spec.calendar == ("*-*-* 03:00:00",)
|
||||
|
||||
|
||||
def test_wizard_edit_keeps_changes_optional(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("sys.stdin", FakeStdin(isatty=True))
|
||||
job = _systemd_job()
|
||||
args = build_parser().parse_args(["edit", "backup", "-i"])
|
||||
prompter, _ = make_prompter(["n", "n", "n"])
|
||||
args, tail = _wizard_edit(job, args, [], prompter)
|
||||
spec = _spec_for_edit(job, args, tail)
|
||||
assert spec.calendar == ("*-*-* 02:00:00",)
|
||||
assert tail == []
|
||||
Reference in New Issue
Block a user