Skip to content

Commit d77c033

Browse files
committed
test(main): add port conflict scenario tests to reach 85% coverage
- 7 tests for main() port conflict handling: CLI server kill/decline/fail/EOF, non-CLI process, unknown owner, stale CLI PID with different occupant
1 parent 978a532 commit d77c033

1 file changed

Lines changed: 171 additions & 0 deletions

File tree

Tools/WebServer/tests/test_main.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,5 +812,176 @@ def test_pyserial_detected_via_metadata(self):
812812
mock_dist.assert_called_with("pyserial")
813813

814814

815+
class TestMainPortConflict(unittest.TestCase):
816+
"""Test main() port conflict handling with get_port_owner and CLI server detection."""
817+
818+
def _mock_args(self, **overrides):
819+
defaults = dict(
820+
host="0.0.0.0",
821+
port=5500,
822+
debug=False,
823+
skip_port_check=False,
824+
no_browser=True,
825+
no_auth=True,
826+
)
827+
defaults.update(overrides)
828+
return Mock(**defaults)
829+
830+
@patch("main.create_app")
831+
@patch("main.restore_state")
832+
@patch("main.check_port_available", return_value=False)
833+
@patch(
834+
"main.get_port_owner",
835+
return_value={"pid": 100, "name": "python", "cmdline": "python main.py"},
836+
)
837+
@patch("main.parse_args")
838+
def test_port_conflict_non_cli_process(
839+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
840+
):
841+
"""Port occupied by non-CLI process → exit with options."""
842+
mock_args.return_value = self._mock_args()
843+
with patch("cli.server_proxy.get_cli_server_pid", return_value=None):
844+
with self.assertRaises(SystemExit) as cm:
845+
main.main()
846+
self.assertEqual(cm.exception.code, 1)
847+
mock_create.assert_not_called()
848+
849+
@patch("main.create_app")
850+
@patch("main.restore_state")
851+
@patch("main.check_port_available", return_value=False)
852+
@patch(
853+
"main.get_port_owner",
854+
return_value={
855+
"pid": 200,
856+
"name": "python",
857+
"cmdline": "python main.py --no-browser",
858+
},
859+
)
860+
@patch("main.parse_args")
861+
def test_port_conflict_cli_server_user_accepts(
862+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
863+
):
864+
"""Port occupied by CLI server, user answers Y → kill and continue."""
865+
mock_args.return_value = self._mock_args()
866+
mock_app = Mock()
867+
mock_create.return_value = mock_app
868+
with patch("cli.server_proxy.get_cli_server_pid", return_value=200), patch(
869+
"cli.server_proxy.stop_cli_server",
870+
return_value={"success": True, "message": "done"},
871+
), patch("builtins.input", return_value="Y"), patch("time.sleep"), patch(
872+
"main.threading.Timer"
873+
):
874+
# Should NOT exit — continues to start server
875+
main.main()
876+
mock_create.assert_called_once()
877+
878+
@patch("main.create_app")
879+
@patch("main.restore_state")
880+
@patch("main.check_port_available", return_value=False)
881+
@patch(
882+
"main.get_port_owner",
883+
return_value={
884+
"pid": 200,
885+
"name": "python",
886+
"cmdline": "python main.py --no-browser",
887+
},
888+
)
889+
@patch("main.parse_args")
890+
def test_port_conflict_cli_server_user_declines(
891+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
892+
):
893+
"""Port occupied by CLI server, user answers n → abort."""
894+
mock_args.return_value = self._mock_args()
895+
with patch("cli.server_proxy.get_cli_server_pid", return_value=200), patch(
896+
"builtins.input", return_value="n"
897+
):
898+
with self.assertRaises(SystemExit) as cm:
899+
main.main()
900+
self.assertEqual(cm.exception.code, 0)
901+
mock_create.assert_not_called()
902+
903+
@patch("main.create_app")
904+
@patch("main.restore_state")
905+
@patch("main.check_port_available", return_value=False)
906+
@patch(
907+
"main.get_port_owner",
908+
return_value={
909+
"pid": 200,
910+
"name": "python",
911+
"cmdline": "python main.py --no-browser",
912+
},
913+
)
914+
@patch("main.parse_args")
915+
def test_port_conflict_cli_server_stop_fails(
916+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
917+
):
918+
"""Port occupied by CLI server, user answers Y but stop fails → exit 1."""
919+
mock_args.return_value = self._mock_args()
920+
with patch("cli.server_proxy.get_cli_server_pid", return_value=200), patch(
921+
"cli.server_proxy.stop_cli_server",
922+
return_value={"success": False, "error": "fail"},
923+
), patch("builtins.input", return_value="Y"):
924+
with self.assertRaises(SystemExit) as cm:
925+
main.main()
926+
self.assertEqual(cm.exception.code, 1)
927+
928+
@patch("main.create_app")
929+
@patch("main.restore_state")
930+
@patch("main.check_port_available", return_value=False)
931+
@patch(
932+
"main.get_port_owner",
933+
return_value={
934+
"pid": 200,
935+
"name": "python",
936+
"cmdline": "python main.py --no-browser",
937+
},
938+
)
939+
@patch("main.parse_args")
940+
def test_port_conflict_cli_server_eof_on_input(
941+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
942+
):
943+
"""Port occupied by CLI server, EOFError on input → abort."""
944+
mock_args.return_value = self._mock_args()
945+
with patch("cli.server_proxy.get_cli_server_pid", return_value=200), patch(
946+
"builtins.input", side_effect=EOFError
947+
):
948+
with self.assertRaises(SystemExit) as cm:
949+
main.main()
950+
self.assertEqual(cm.exception.code, 0)
951+
952+
@patch("main.create_app")
953+
@patch("main.restore_state")
954+
@patch("main.check_port_available", return_value=False)
955+
@patch("main.get_port_owner", return_value=None)
956+
@patch("main.parse_args")
957+
def test_port_conflict_unknown_owner(
958+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
959+
):
960+
"""Port occupied but owner unknown → exit with generic message."""
961+
mock_args.return_value = self._mock_args()
962+
with patch("cli.server_proxy.get_cli_server_pid", return_value=None):
963+
with self.assertRaises(SystemExit) as cm:
964+
main.main()
965+
self.assertEqual(cm.exception.code, 1)
966+
967+
@patch("main.create_app")
968+
@patch("main.restore_state")
969+
@patch("main.check_port_available", return_value=False)
970+
@patch(
971+
"main.get_port_owner",
972+
return_value={"pid": 100, "name": "node", "cmdline": "node server.js"},
973+
)
974+
@patch("main.parse_args")
975+
def test_port_conflict_non_cli_with_stale_cli_pid(
976+
self, mock_args, mock_owner, mock_check, mock_restore, mock_create
977+
):
978+
"""Port occupied by non-CLI process but stale CLI PID exists → show both options."""
979+
mock_args.return_value = self._mock_args()
980+
with patch("cli.server_proxy.get_cli_server_pid", return_value=999):
981+
with self.assertRaises(SystemExit) as cm:
982+
main.main()
983+
self.assertEqual(cm.exception.code, 1)
984+
985+
815986
if __name__ == "__main__":
816987
unittest.main()

0 commit comments

Comments
 (0)