| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -26,7 +26,9 @@ the documented behavior never drifts from the code. | |||
| 26 | 26 | simulator. Flags: `--device` (target a specific device by identifier | |
| 27 | 27 | or name), `--prepare-only`, `--hot-reload`, `--no-logs`. | |
| 28 | 28 | - `pn logs android|ios`: stream logs from the running app without | |
| 29 | - rebuilding. | ||
| 29 | + rebuilding. Flag: `--device` (target a specific device by identifier | ||
| 30 | + or name, same as `pn run`). Physical iOS devices aren't supported for | ||
| 31 | + log streaming; use Console.app or Xcode > Devices and Simulators. | ||
| 30 | 32 | - `pn build android|ios`: build distributable artifacts (release by | |
| 31 | 33 | default). Flags: `--debug` for the debug variant, `--upload` to send | |
| 32 | 34 | an iOS release build to App Store Connect. See | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,8 +12,8 @@ | |||
| 12 | 12 | simulators, as a table or as JSON with `--json`. | |
| 13 | 13 | - `pn run android|ios [--device D]`: stage + build + install + launch on | |
| 14 | 14 | a device, emulator, or simulator, with optional on-device hot reload. | |
| 15 | - - `pn logs android|ios`: stream logs from the running app without | ||
| 16 | - rebuilding. | ||
| 15 | + - `pn logs android|ios [--device D]`: stream logs from the running app | ||
| 16 | + without rebuilding. | ||
| 17 | 17 | - `pn build android|ios`: produce standalone artifacts (signed APK/AAB, | |
| 18 | 18 | or an iOS archive/IPA, optionally uploaded to App Store Connect). | |
| 19 | 19 | - `pn app-id android|ios`: print the resolved application/bundle id | |
@@ -626,10 +626,15 @@ def logs_command(args: argparse.Namespace) -> None: | |||
| 626 | 626 | """Stream logs from the running app without rebuilding. | |
| 627 | 627 | ||
| 628 | 628 | Args: | |
| 629 | - args: Parsed namespace (``platform``). | ||
| 629 | + args: Parsed namespace (``platform``, ``device``). | ||
| 630 | 630 | """ | |
| 631 | 631 | platform: str = args.platform | |
| 632 | + device = _resolve_device(platform, getattr(args, "device", None)) | ||
| 632 | 633 | if platform == "android": | |
| 634 | + if device is not None: | ||
| 635 | + # Both adb and logcat below honor ANDROID_SERIAL, so exporting | ||
| 636 | + # it targets the whole log stream at the chosen device. | ||
| 637 | + os.environ["ANDROID_SERIAL"] = device.identifier | ||
| 633 | 638 | proc = _start_android_log_stream() | |
| 634 | 639 | if proc is None: | |
| 635 | 640 | sys.exit(1) | |
@@ -643,8 +648,12 @@ def logs_command(args: argparse.Namespace) -> None: | |||
| 643 | 648 | ||
| 644 | 649 | # iOS: relaunch the app on the booted simulator with a console PTY so | |
| 645 | 650 | # Python's stdout/stderr stream to this terminal. | |
| 651 | + if device is not None and device.kind == "device": | ||
| 652 | + print("For a physical device, use Console.app or Xcode > Devices and Simulators.") | ||
| 653 | + sys.exit(1) | ||
| 646 | 654 | config = _load_config_or_exit() | |
| 647 | - proc = _start_ios_log_stream(config.bundle_id) | ||
| 655 | + udid = device.identifier if device is not None else None | ||
| 656 | + proc = _start_ios_log_stream(config.bundle_id, udid=udid) | ||
| 648 | 657 | if proc is None: | |
| 649 | 658 | print("For a physical device, use Console.app or Xcode > Devices and Simulators.") | |
| 650 | 659 | sys.exit(1) | |
@@ -762,16 +771,19 @@ def _select_ios_simulator() -> Optional[str]: | |||
| 762 | 771 | return None | |
| 763 | 772 | ||
| 764 | 773 | ||
| 765 | - def _start_ios_log_stream(bundle_id: str) -> Optional[subprocess.Popen]: | ||
| 774 | + def _start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> Optional[subprocess.Popen]: | ||
| 766 | 775 | """Re-launch the iOS app with a console PTY so its stdio streams here. | |
| 767 | 776 | ||
| 768 | 777 | Args: | |
| 769 | 778 | bundle_id: The app's bundle identifier. | |
| 779 | + udid: A specific simulator UDID to target. Falls back to the | ||
| 780 | + booted simulator when not given. | ||
| 770 | 781 | ||
| 771 | 782 | Returns: | |
| 772 | 783 | The launched process, or ``None`` when no simulator is booted. | |
| 773 | 784 | """ | |
| 774 | - udid = _booted_ios_udid() | ||
| 785 | + if udid is None: | ||
| 786 | + udid = _booted_ios_udid() | ||
| 775 | 787 | if udid is None: | |
| 776 | 788 | print("Note: no booted iOS Simulator found; skipping log streaming.") | |
| 777 | 789 | return None | |
@@ -1044,6 +1056,12 @@ def _build_parser() -> argparse.ArgumentParser: | |||
| 1044 | 1056 | ||
| 1045 | 1057 | parser_logs = subparsers.add_parser("logs", help="Stream logs from the running app") | |
| 1046 | 1058 | parser_logs.add_argument("platform", choices=["android", "ios"]) | |
| 1059 | + parser_logs.add_argument( | ||
| 1060 | + "--device", | ||
| 1061 | + "-d", | ||
| 1062 | + help="Target device: an identifier or name from 'pn devices' " | ||
| 1063 | + "(physical iOS devices aren't supported for log streaming)", | ||
| 1064 | + ) | ||
| 1047 | 1065 | parser_logs.set_defaults(func=logs_command) | |
| 1048 | 1066 | ||
| 1049 | 1067 | parser_build = subparsers.add_parser("build", help="Build distributable artifacts") | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -620,6 +620,132 @@ def test_devices_json_serializes_awkward_field_values( | |||
| 620 | 620 | } | |
| 621 | 621 | ||
| 622 | 622 | ||
| 623 | + class _FakeCompletedProc: | ||
| 624 | + """Stand-in for subprocess.Popen that returns immediately from wait().""" | ||
| 625 | + | ||
| 626 | + def wait(self) -> int: | ||
| 627 | + return 0 | ||
| 628 | + | ||
| 629 | + | ||
| 630 | + def test_logs_command_android_sets_android_serial_for_device( | ||
| 631 | + monkeypatch: pytest.MonkeyPatch, | ||
| 632 | + ) -> None: | ||
| 633 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES)) | ||
| 634 | + monkeypatch.delenv("ANDROID_SERIAL", raising=False) | ||
| 635 | + monkeypatch.setattr(pn_cli, "_start_android_log_stream", lambda: _FakeCompletedProc()) | ||
| 636 | + | ||
| 637 | + pn_cli.logs_command(argparse.Namespace(platform="android", device="Pixel")) | ||
| 638 | + | ||
| 639 | + # pop() rather than a plain lookup: monkeypatch.delenv() on a variable that | ||
| 640 | + # was never set has nothing to restore, so the value the command exported | ||
| 641 | + # would otherwise leak into the rest of the session. | ||
| 642 | + assert os.environ.pop("ANDROID_SERIAL") == "R5CT12345XYZ" | ||
| 643 | + | ||
| 644 | + | ||
| 645 | + def test_logs_command_android_no_device_leaves_android_serial_unset( | ||
| 646 | + monkeypatch: pytest.MonkeyPatch, | ||
| 647 | + ) -> None: | ||
| 648 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES)) | ||
| 649 | + monkeypatch.delenv("ANDROID_SERIAL", raising=False) | ||
| 650 | + monkeypatch.setattr(pn_cli, "_start_android_log_stream", lambda: _FakeCompletedProc()) | ||
| 651 | + | ||
| 652 | + pn_cli.logs_command(argparse.Namespace(platform="android", device=None)) | ||
| 653 | + | ||
| 654 | + assert "ANDROID_SERIAL" not in os.environ | ||
| 655 | + | ||
| 656 | + | ||
| 657 | + def test_logs_command_ios_passes_resolved_udid(monkeypatch: pytest.MonkeyPatch) -> None: | ||
| 658 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES)) | ||
| 659 | + monkeypatch.setattr( | ||
| 660 | + pn_cli, "_load_config_or_exit", lambda project_dir=None: argparse.Namespace(bundle_id="com.example.app") | ||
| 661 | + ) | ||
| 662 | + captured: Dict[str, Optional[str]] = {} | ||
| 663 | + | ||
| 664 | + def _fake_start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> _FakeCompletedProc: | ||
| 665 | + captured["bundle_id"] = bundle_id | ||
| 666 | + captured["udid"] = udid | ||
| 667 | + return _FakeCompletedProc() | ||
| 668 | + | ||
| 669 | + monkeypatch.setattr(pn_cli, "_start_ios_log_stream", _fake_start_ios_log_stream) | ||
| 670 | + | ||
| 671 | + pn_cli.logs_command(argparse.Namespace(platform="ios", device="iPhone 17")) | ||
| 672 | + | ||
| 673 | + assert captured == {"bundle_id": "com.example.app", "udid": "ABC-123"} | ||
| 674 | + | ||
| 675 | + | ||
| 676 | + def test_logs_command_ios_no_device_falls_back_to_booted( | ||
| 677 | + monkeypatch: pytest.MonkeyPatch, | ||
| 678 | + ) -> None: | ||
| 679 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES)) | ||
| 680 | + monkeypatch.setattr( | ||
| 681 | + pn_cli, "_load_config_or_exit", lambda project_dir=None: argparse.Namespace(bundle_id="com.example.app") | ||
| 682 | + ) | ||
| 683 | + captured: Dict[str, Optional[str]] = {} | ||
| 684 | + | ||
| 685 | + def _fake_start_ios_log_stream(bundle_id: str, *, udid: Optional[str] = None) -> _FakeCompletedProc: | ||
| 686 | + captured["udid"] = udid | ||
| 687 | + return _FakeCompletedProc() | ||
| 688 | + | ||
| 689 | + monkeypatch.setattr(pn_cli, "_start_ios_log_stream", _fake_start_ios_log_stream) | ||
| 690 | + | ||
| 691 | + pn_cli.logs_command(argparse.Namespace(platform="ios", device=None)) | ||
| 692 | + | ||
| 693 | + assert captured["udid"] is None | ||
| 694 | + | ||
| 695 | + | ||
| 696 | + def test_logs_command_ios_physical_device_prints_console_hint( | ||
| 697 | + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] | ||
| 698 | + ) -> None: | ||
| 699 | + physical = Device("ios", "device", "PHYS-1", "Owen's iPhone", "iOS 26.4", "connected") | ||
| 700 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices([physical])) | ||
| 701 | + | ||
| 702 | + with pytest.raises(SystemExit) as exc_info: | ||
| 703 | + pn_cli.logs_command(argparse.Namespace(platform="ios", device="Owen's iPhone")) | ||
| 704 | + | ||
| 705 | + assert exc_info.value.code == 1 | ||
| 706 | + assert "Console.app" in capsys.readouterr().out | ||
| 707 | + | ||
| 708 | + | ||
| 709 | + def test_logs_command_ios_no_simulator_prints_console_hint( | ||
| 710 | + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] | ||
| 711 | + ) -> None: | ||
| 712 | + # Without --device and with no booted simulator, the user may well be | ||
| 713 | + # holding a physical device, so the Console.app pointer must still print. | ||
| 714 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES)) | ||
| 715 | + monkeypatch.setattr( | ||
| 716 | + pn_cli, "_load_config_or_exit", lambda project_dir=None: argparse.Namespace(bundle_id="com.example.app") | ||
| 717 | + ) | ||
| 718 | + monkeypatch.setattr(pn_cli, "_start_ios_log_stream", lambda bundle_id, *, udid=None: None) | ||
| 719 | + | ||
| 720 | + with pytest.raises(SystemExit) as exc_info: | ||
| 721 | + pn_cli.logs_command(argparse.Namespace(platform="ios", device=None)) | ||
| 722 | + | ||
| 723 | + assert exc_info.value.code == 1 | ||
| 724 | + assert "Console.app" in capsys.readouterr().out | ||
| 725 | + | ||
| 726 | + | ||
| 727 | + def test_logs_command_bad_device_query_exits_with_hint( | ||
| 728 | + monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str] | ||
| 729 | + ) -> None: | ||
| 730 | + monkeypatch.setattr(pn_cli.devices_mod, "list_devices", _fake_list_devices(_FAKE_DEVICES)) | ||
| 731 | + | ||
| 732 | + with pytest.raises(SystemExit) as exc_info: | ||
| 733 | + pn_cli.logs_command(argparse.Namespace(platform="android", device="nope")) | ||
| 734 | + | ||
| 735 | + assert exc_info.value.code == 1 | ||
| 736 | + assert "no android device matches 'nope'" in capsys.readouterr().out | ||
| 737 | + | ||
| 738 | + | ||
| 739 | + def test_logs_device_flag_is_wired_through_argparse(tmp_path: Path) -> None: | ||
| 740 | + # The tests above call logs_command() directly; this one proves the | ||
| 741 | + # subparser actually accepts --device and routes it through. | ||
| 742 | + env = {**os.environ, "PATH": str(tmp_path)} | ||
| 743 | + result = run_pn(["logs", "android", "--device", "nope"], str(tmp_path), env=env) | ||
| 744 | + | ||
| 745 | + assert result.returncode == 1 | ||
| 746 | + assert "no android device matches 'nope'" in result.stdout | ||
| 747 | + | ||
| 748 | + | ||
| 623 | 749 | def test_hot_reload_manifest_payload_maps_files_to_modules(tmp_path: Path) -> None: | |
| 624 | 750 | app_dir = tmp_path / "app" | |
| 625 | 751 | app_dir.mkdir() | |
| Back | FazBrowse Home | New Git URL |
0 commit comments