Files
schedls/tests/unit/test_prompt.py
T
mdaleo404 aded37d491
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
Initial code commit
2026-09-25 11:16:19 +01:00

204 lines
7.0 KiB
Python

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 == []