FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat(cli): add --device flag to pn logs by crux200 · Pull Request #67 · pythonnative/pythonnative · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .md  (1) .py  (2) All 2 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
4 changes: 3 additions & 1 deletion docs/api/cli.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ the documented behavior never drifts from the code.
simulator. Flags: `--device` (target a specific device by identifier
or name), `--prepare-only`, `--hot-reload`, `--no-logs`.
- `pn logs android|ios`: stream logs from the running app without
rebuilding.
rebuilding. Flag: `--device` (target a specific device by identifier
or name, same as `pn run`). Physical iOS devices aren't supported for
log streaming; use Console.app or Xcode > Devices and Simulators.
- `pn build android|ios`: build distributable artifacts (release by
default). Flags: `--debug` for the debug variant, `--upload` to send
an iOS release build to App Store Connect. See
Expand Down
30 changes: 24 additions & 6 deletions src/pythonnative/cli/pn.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
simulators, as a table or as JSON with `--json`.
- `pn run android|ios [--device D]`: stage + build + install + launch on
a device, emulator, or simulator, with optional on-device hot reload.
- `pn logs android|ios`: stream logs from the running app without
rebuilding.
- `pn logs android|ios [--device D]`: stream logs from the running app
without rebuilding.
- `pn build android|ios`: produce standalone artifacts (signed APK/AAB,
or an iOS archive/IPA, optionally uploaded to App Store Connect).
- `pn app-id android|ios`: print the resolved application/bundle id
Expand Down Expand Up @@ -627,10 +627,15 @@ def logs_command(args: argparse.Namespace) -> None:
"""Stream logs from the running app without rebuilding.

Args:
args: Parsed namespace (``platform``).
args: Parsed namespace (``platform``, ``device``).
"""
platform: str = args.platform
device = _resolve_device(platform, getattr(args, "device", None))
if platform == "android":
if device is not None:
# Both adb and logcat below honor ANDROID_SERIAL, so exporting
# it targets the whole log stream at the chosen device.
os.environ["ANDROID_SERIAL"] = device.identifier
proc = _start_android_log_stream()
if proc is None:
sys.exit(1)
Expand All @@ -644,8 +649,12 @@ def logs_command(args: argparse.Namespace) -> None:

# iOS: relaunch the app on the booted simulator with a console PTY so
# Python's stdout/stderr stream to this terminal.
if device is not None and device.kind == "device":
print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
sys.exit(1)
config = _load_config_or_exit()
proc = _start_ios_log_stream(config.bundle_id)
udid = device.identifier if device is not None else None
proc = _start_ios_log_stream(config.bundle_id, udid=udid)
if proc is None:
print("For a physical device, use Console.app or Xcode > Devices and Simulators.")
sys.exit(1)
Expand Down Expand Up @@ -763,16 +772,19 @@ def _select_ios_simulator() -> Optional[str]:
return None


def _start_ios_log_stream(bundle_id: str) -> Optional[subprocess.Popen]:
def _start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> Optional[subprocess.Popen]:
"""Re-launch the iOS app with a console PTY so its stdio streams here.

Args:
bundle_id: The app's bundle identifier.
udid: A specific simulator UDID to target. Falls back to the
booted simulator when not given.

Returns:
The launched process, or ``None`` when no simulator is booted.
"""
udid = _booted_ios_udid()
if udid is None:
udid = _booted_ios_udid()
if udid is None:
print("Note: no booted iOS Simulator found; skipping log streaming.")
return None
Expand Down Expand Up @@ -1045,6 +1057,12 @@ def _build_parser() -> argparse.ArgumentParser:

parser_logs = subparsers.add_parser("logs", help="Stream logs from the running app")
parser_logs.add_argument("platform", choices=["android", "ios"])
parser_logs.add_argument(
"--device",
"-d",
help="Target device: an identifier or name from 'pn devices' "
"(physical iOS devices aren't supported for log streaming)",
)
parser_logs.set_defaults(func=logs_command)

parser_build = subparsers.add_parser("build", help="Build distributable artifacts")
Expand Down
126 changes: 126 additions & 0 deletions tests/test_cli.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,132 @@ def test_devices_json_serializes_awkward_field_values(
}


class _FakeCompletedProc:
"""Stand-in for subprocess.Popen that returns immediately from wait()."""

def wait(self) -> int:
return 0


def test_logs_command_android_sets_android_serial_for_device(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
monkeypatch.delenv("ANDROID_SERIAL", raising=False)
monkeypatch.setattr(pn_cli, "_start_android_log_stream", lambda: _FakeCompletedProc())

pn_cli.logs_command(argparse.Namespace(platform="android", device="Pixel"))

# pop() rather than a plain lookup: monkeypatch.delenv() on a variable that
# was never set has nothing to restore, so the value the command exported
# would otherwise leak into the rest of the session.
assert os.environ.pop("ANDROID_SERIAL") == "R5CT12345XYZ"


def test_logs_command_android_no_device_leaves_android_serial_unset(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
monkeypatch.delenv("ANDROID_SERIAL", raising=False)
monkeypatch.setattr(pn_cli, "_start_android_log_stream", lambda: _FakeCompletedProc())

pn_cli.logs_command(argparse.Namespace(platform="android", device=None))

assert "ANDROID_SERIAL" not in os.environ


def test_logs_command_ios_passes_resolved_udid(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
monkeypatch.setattr(
pn_cli, "_load_config_or_exit", lambda project_dir=None: argparse.Namespace(bundle_id="com.example.app")
)
captured: Dict[str, Optional[str]] = {}

def _fake_start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> _FakeCompletedProc:
captured["bundle_id"] = bundle_id
captured["udid"] = udid
return _FakeCompletedProc()

monkeypatch.setattr(pn_cli, "_start_ios_log_stream", _fake_start_ios_log_stream)

pn_cli.logs_command(argparse.Namespace(platform="ios", device="iPhone 17"))

assert captured == {"bundle_id": "com.example.app", "udid": "ABC-123"}


def test_logs_command_ios_no_device_falls_back_to_booted(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
monkeypatch.setattr(
pn_cli, "_load_config_or_exit", lambda project_dir=None: argparse.Namespace(bundle_id="com.example.app")
)
captured: Dict[str, Optional[str]] = {}

def _fake_start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> _FakeCompletedProc:
captured["udid"] = udid
return _FakeCompletedProc()

monkeypatch.setattr(pn_cli, "_start_ios_log_stream", _fake_start_ios_log_stream)

pn_cli.logs_command(argparse.Namespace(platform="ios", device=None))

assert captured["udid"] is None


def test_logs_command_ios_physical_device_prints_console_hint(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
physical = Device("ios", "device", "PHYS-1", "Owen's iPhone", "iOS 26.4", "connected")
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices([physical]))

with pytest.raises(SystemExit) as exc_info:
pn_cli.logs_command(argparse.Namespace(platform="ios", device="Owen's iPhone"))

assert exc_info.value.code == 1
assert "Console.app" in capsys.readouterr().out


def test_logs_command_ios_no_simulator_prints_console_hint(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
# Without --device and with no booted simulator, the user may well be
# holding a physical device, so the Console.app pointer must still print.
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))
monkeypatch.setattr(
pn_cli, "_load_config_or_exit", lambda project_dir=None: argparse.Namespace(bundle_id="com.example.app")
)
monkeypatch.setattr(pn_cli, "_start_ios_log_stream", lambda bundle_id, *, udid=None: None)

with pytest.raises(SystemExit) as exc_info:
pn_cli.logs_command(argparse.Namespace(platform="ios", device=None))

assert exc_info.value.code == 1
assert "Console.app" in capsys.readouterr().out


def test_logs_command_bad_device_query_exits_with_hint(
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
) -> None:
monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES))

with pytest.raises(SystemExit) as exc_info:
pn_cli.logs_command(argparse.Namespace(platform="android", device="nope"))

assert exc_info.value.code == 1
assert "no android device matches 'nope'" in capsys.readouterr().out


def test_logs_device_flag_is_wired_through_argparse(tmp_path: Path) -> None:
# The tests above call logs_command() directly; this one proves the
# subparser actually accepts --device and routes it through.
env = {**os.environ, "PATH": str(tmp_path)}
result = run_pn(["logs", "android", "--device", "nope"], str(tmp_path), env=env)

assert result.returncode == 1
assert "no android device matches 'nope'" in result.stdout


def test_hot_reload_manifest_payload_maps_files_to_modules(tmp_path: Path) -> None:
app_dir = tmp_path / "app"
app_dir.mkdir()
Expand Down
Loading

Back | FazBrowse Home | New Git URL