Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion skillopt_sleep/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,8 @@ def cmd_schedule(args) -> int:
project = cfg.get("invoked_project") or os.getcwd()
ok, msg = schedule(project, backend=cfg.get("backend", "mock"),
hour=args.hour, minute=args.minute,
extra=("--auto-adopt" if getattr(args, "auto_adopt", False) else ""))
extra=("--auto-adopt" if getattr(args, "auto_adopt", False) else ""),
scope=args.scope or "invoked")
print("[sleep] " + _display_value(msg))
cur = list_scheduled()
if cur:
Expand Down
12 changes: 8 additions & 4 deletions skillopt_sleep/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ def _list_win_tasks() -> List[str]:
return []


def _runner_cmd(project: str, backend: str, extra: str, python: str) -> str:
def _runner_cmd(project: str, backend: str, extra: str, python: str,
scope: str = "invoked") -> str:
if scope not in ("invoked", "all"):
raise ValueError(f"scope must be 'invoked' or 'all', got {scope!r}")
_validate_scheduler_text("project", project)
_validate_scheduler_text("python", python)
_validate_scheduler_text("repository", _repo_root())
Expand All @@ -120,7 +123,7 @@ def _runner_cmd(project: str, backend: str, extra: str, python: str) -> str:
"--project",
project,
"--scope",
"invoked",
scope,
"--backend",
backend,
*shlex.split(extra),
Expand Down Expand Up @@ -184,7 +187,8 @@ def _project_marker(project: str) -> str:


def schedule(project: str, *, backend: str = "mock", hour: int = 3, minute: int = 17,
extra: str = "", python: Optional[str] = None) -> Tuple[bool, str]:
extra: str = "", python: Optional[str] = None,
scope: str = "invoked") -> Tuple[bool, str]:
"""Install (or replace) the nightly entry for ``project``.

Returns (installed, message). If the scheduler backend is unavailable, installed=False and
Expand All @@ -197,7 +201,7 @@ def schedule(project: str, *, backend: str = "mock", hour: int = 3, minute: int
project = os.path.abspath(project)
python = python or sys.executable or "python3"
try:
runner_cmd = _runner_cmd(project, backend, extra, python)
runner_cmd = _runner_cmd(project, backend, extra, python, scope)
except (ValueError, OSError) as exc:
return False, f"Refusing unsafe scheduler configuration: {exc}"

Expand Down
18 changes: 18 additions & 0 deletions tests/test_sleep_scheduler_safety.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ def test_posix_runner_quotes_every_path_and_argument(self):
self.assertIn(shlex.quote("/tmp/python with spaces"), command)
self.assertNotIn("$(", command)

@unittest.skipIf(os.name == "nt", "POSIX runner quoting is not used on Windows")
def test_scheduled_run_uses_the_requested_scope(self):
with tempfile.TemporaryDirectory() as project:
default = scheduler._runner_cmd(project, "mock", "", "python3")
invoked = scheduler._runner_cmd(project, "mock", "", "python3", "invoked")
everything = scheduler._runner_cmd(project, "mock", "", "python3", "all")
self.assertEqual(default, invoked)
for command, expected in ((default, "invoked"), (everything, "all")):
tokens = shlex.split(command)
self.assertEqual(tokens[tokens.index("--scope") + 1], expected)

def test_unknown_scope_is_refused_before_crontab_write(self):
with mock.patch.object(scheduler, "_write_crontab") as write:
ok, message = scheduler.schedule("/tmp/project", scope="everything")
self.assertFalse(ok)
self.assertIn("scope must be", message)
write.assert_not_called()

def test_control_characters_are_refused_before_crontab_write(self):
with mock.patch.object(scheduler, "_write_crontab") as write:
ok, message = scheduler.schedule("/tmp/project\n* * * * * injected")
Expand Down