Skip to content
Merged
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 pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
dev = [
"black>=25.1.0",
"coveralls>=4.0.1",
"ipython>=9.4.0",
"mypy>=1.17.1",
"pip-audit>=2.9.0",
"pre-commit>=4.3.0",
Expand Down Expand Up @@ -93,4 +94,4 @@ fixture-parentheses = false
max-doc-length = 90

[tool.ruff.lint.pydocstyle]
convention = "google"
convention = "google"
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from click.testing import CliRunner

import launcher.cli as cli_module


@pytest.fixture(autouse=True)
def _test_env(monkeypatch):
Expand All @@ -11,3 +13,17 @@ def _test_env(monkeypatch):
@pytest.fixture
def runner():
return CliRunner()


@pytest.fixture
def mocked_git_clone(monkeypatch):
"""Simulate a successful git clone by creating the target directory."""
created: list[str] = []

def _fake_clone(target, repo: str, branch: str | None = None):
target.mkdir(parents=True, exist_ok=True)
created.append(str(target))

monkeypatch.setattr(cli_module, "clone_notebook_repository", _fake_clone)

return created
159 changes: 158 additions & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,163 @@
from launcher.cli import cli
# ruff: noqa: FBT001, S104

import subprocess
from pathlib import Path
from unittest import mock

import pytest

from launcher.cli import (
cli,
prepare_run_command,
resolve_notebook_directory,
resolve_notebook_path,
)


def test_resolve_notebook_directory_mount(tmp_path: Path) -> None:
notebook_dir = tmp_path / "nb"
notebook_dir.mkdir()
resolved_notebook_dir = resolve_notebook_directory(mount=str(notebook_dir))
assert resolved_notebook_dir == notebook_dir


def test_resolve_notebook_directory_missing_mount(tmp_path: Path) -> None:
missing = tmp_path / "does-not-exist"
with pytest.raises(FileNotFoundError):
resolve_notebook_directory(mount=str(missing))


def test_resolve_notebook_directory_repo_creates_dir_via_git_clone(
tmp_path: Path, mocked_git_clone
) -> None:
notebook_dir = resolve_notebook_directory(
mount=None, repo="https://example.com/x.git", repo_branch="main"
)

assert notebook_dir.is_dir()
assert Path(mocked_git_clone[0]) == notebook_dir


def test_resolve_notebook_path_exists(tmp_path: Path) -> None:
notebook_file = tmp_path / "notebook.py"
notebook_file.write_text("print('hi')\n")
resolved_path = resolve_notebook_path(tmp_path, "notebook.py")
assert resolved_path == notebook_file


def test_resolve_notebook_path_missing(tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
resolve_notebook_path(tmp_path, "missing.py")


@pytest.mark.parametrize(
("has_requirements", "token"),
[
(False, None),
(False, "secret-token"),
(True, None),
(True, "secret-token"),
],
)
@pytest.mark.parametrize("mode", ["run", "edit"])
def test_prepare_run_command_variants(
tmp_path: Path,
has_requirements: bool,
token: str | None,
mode: str,
) -> None:
requirements = tmp_path / "requirements.txt"
if has_requirements:
requirements.write_text("marimo==0.14.17\n")
requirements_path: Path | None = requirements
else:
requirements_path = None

command = prepare_run_command(
mode=mode,
host="127.0.0.1",
port=8888,
token=token,
notebook_path="notebook.py",
requirements_file=requirements_path,
)

# must start with uv run
assert command[:2] == ["uv", "run"]

# requirements handling
if has_requirements:
assert "--with-requirements" in command
assert str(requirements) in command
assert "--sandbox" not in command
else:
assert "--sandbox" in command
assert "--with-requirements" not in command

# token handling
if token:
assert ["--token", "--token-password", token] == command[-4:-1]
else:
assert "--no-token" in command

# notebook path is last
assert command[-1] == "notebook.py"


def test_cli_no_commands(caplog, runner):
result = runner.invoke(cli, [])
assert result.exit_code == 2


def test_cli_subprocess_run_minimal_required_args_get_defaults_success(runner):
"""Mock subprocess.run to simulate valid notebook run and exit."""
args = ["run", "--mount", "tests/fixtures/inline_deps"]

with mock.patch("launcher.cli.subprocess.run") as mock_subprocess_run:
mock_subprocess_run.return_value = subprocess.CompletedProcess(
args=args, returncode=0
)
_result = runner.invoke(cli, args)

# assert subproces.run has correct working directory of notebook
assert mock_subprocess_run.call_args.kwargs.get("cwd") == "tests/fixtures/inline_deps"

# assert subprocess.run had defaults applied
assert mock_subprocess_run.call_args.args[0] == [
"uv",
"run",
"marimo",
"run",
"--headless",
"--host",
"0.0.0.0",
"--port",
"2718",
"--sandbox",
"--no-token",
"notebook.py",
]


def test_cli_subprocess_run_missing_mount_or_repo_args_error(runner):
args = ["run"]

result = runner.invoke(cli, args)

assert result.exit_code == 1
assert (
str(result.exception)
== "either --mount/NOTEBOOK_MOUNT or --repo/NOTEBOOK_REPOSITORY must be provided"
)


def test_cli_subprocess_run_bad_notebook_path_error(runner):
args = ["run", "--mount", "tests/fixtures/inline_deps", "--path", "bad-notebook.py"]

result = runner.invoke(cli, args)

assert result.exit_code == 1
assert (
str(result.exception)
== "notebook path not found: tests/fixtures/inline_deps/bad-notebook.py"
)
Loading