blob: 7820219a4a18884e13cb148689548506c71477d8 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import json
import unittest
from io import StringIO
from unittest.mock import AsyncMock, Mock, patch
from cli.cli import main
from cli.commands.break_cmd import resolve_path
from daemon_manager.manager import (
DaemonAlreadyRunningError,
DaemonConnectionError,
DaemonCrashError,
DaemonHandshakeError,
DaemonStartupTimeoutError,
)
from shared.protocol.attach import AttachRequest
from shared.protocol.break_request import BreakRequest
from shared.protocol.continue_request import ContinueRequest
from shared.protocol.evaluate import EvaluateRequest
from shared.protocol.pause import PauseRequest
from shared.protocol.stack_trace import StackTraceRequest
from shared.protocol.stop import StopRequest
from shared.protocol.threads import ThreadsRequest
from shared.protocol.variables import VariablesRequest
class TestCLI(unittest.IsolatedAsyncioTestCase):
@patch("cli.commands.start.start_daemon")
async def test_start_command(self, mock_start: Mock) -> None:
mock_start.return_value = 0
exit_code = await main(["start"])
self.assertEqual(exit_code, 0)
mock_start.assert_called_once()
@patch("cli.commands.stop.stop_daemon")
async def test_stop_command(self, mock_stop: Mock) -> None:
mock_stop.return_value = 0
exit_code = await main(["stop"])
self.assertEqual(exit_code, 0)
mock_stop.assert_called_once()
@patch("cli.cli.send_command")
async def test_attach_command(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(["attach", "my_process"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(AttachRequest(filter="my_process"))
@patch("cli.cli.make_request")
@patch("cli.cli.send_command")
async def test_attach_command_receives_args(
self, mock_send: Mock, mock_make: Mock
) -> None:
mock_send.return_value = 0
mock_make.return_value = StopRequest() # dummy
await main(["attach", "my_process"])
mock_make.assert_called_once()
args = mock_make.call_args[0][0] # first argument
self.assertIn("filter", args)
self.assertEqual(args["filter"], "my_process")
@patch("cli.cli.send_command")
async def test_threads_command(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(["threads"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(ThreadsRequest())
@patch("cli.cli.send_command")
async def test_json_option_mutual_exclusion(self, mock_send: Mock) -> None:
exit_code = await main(["--json", '{"command": "stop"}', "stop"])
self.assertEqual(exit_code, 1)
mock_send.assert_not_called()
@patch("cli.commands.stop.stop_daemon")
@patch("cli.cli.send_command")
async def test_json_option_valid(
self, mock_send: Mock, mock_stop: Mock
) -> None:
mock_stop.return_value = 0
exit_code = await main(["--json", '{"command": "stop"}'])
self.assertEqual(exit_code, 0)
mock_stop.assert_called_once()
mock_send.assert_not_called()
@patch("cli.cli.send_command")
async def test_json_option_invalid(self, mock_send: Mock) -> None:
exit_code = await main(["--json", '{"command": "invalid"}'])
self.assertEqual(exit_code, 1)
mock_send.assert_not_called()
@patch("cli.cli.send_command")
async def test_json_option_continue(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
[
"--json",
'{"command": "continue", "thread_id": 1, "single_thread": true}',
]
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
ContinueRequest(thread_id=1, single_thread=True)
)
@patch("cli.cli.send_command")
async def test_json_option_pause(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
["--json", '{"command": "pause", "thread_id": 1}']
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(PauseRequest(thread_id=1))
@patch("cli.cli.send_command")
async def test_json_option_stack_trace(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
["--json", '{"command": "stackTrace", "thread_id": 1}']
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(StackTraceRequest(thread_id=1))
@patch("cli.cli.send_command")
async def test_json_option_attach(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
["--json", '{"command": "attach", "filter": "my_process"}']
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(AttachRequest(filter="my_process"))
@patch("cli.commands.start.DaemonManager")
async def test_start_command_errors_formatting(
self, mock_manager_class: Mock
) -> None:
mock_manager = mock_manager_class.return_value
exceptions_to_test = [
DaemonAlreadyRunningError("Daemon socket already exists"),
DaemonConnectionError("Connection failed"),
DaemonCrashError("Daemon exited prematurely"),
DaemonHandshakeError("Protocol version mismatch"),
DaemonStartupTimeoutError("Startup timed out"),
]
for exc in exceptions_to_test:
mock_manager.start = AsyncMock(side_effect=exc)
stderr = StringIO()
with patch("sys.stderr", stderr):
exit_code = await main(["start"])
self.assertEqual(exit_code, 1)
output = json.loads(stderr.getvalue())
self.assertFalse(output["success"])
self.assertEqual(output["message"], str(exc))
@patch("cli.commands.start.DaemonManager")
async def test_start_command_generic_exception_formatting(
self, mock_manager_class: Mock
) -> None:
mock_manager = mock_manager_class.return_value
mock_manager.start = AsyncMock(
side_effect=RuntimeError("Unexpected error")
)
stderr = StringIO()
with patch("sys.stderr", stderr):
exit_code = await main(["start"])
self.assertEqual(exit_code, 1)
output = json.loads(stderr.getvalue())
self.assertFalse(output["success"])
self.assertIn(
"Failed to start daemon: Unexpected error", output["message"]
)
@patch("cli.cli.send_command")
async def test_variables_command(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(["variables", "1", "--frame-index", "2"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
VariablesRequest(thread_id=1, frame_index=2)
)
@patch("cli.cli.send_command")
async def test_variables_command_default_frame_index(
self, mock_send: Mock
) -> None:
mock_send.return_value = 0
exit_code = await main(["variables", "1"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
VariablesRequest(thread_id=1, frame_index=0)
)
@patch("cli.cli.send_command")
async def test_locals_alias_command(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(["locals", "1", "--frame-index", "2"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
VariablesRequest(thread_id=1, frame_index=2)
)
@patch("cli.cli.send_command")
async def test_json_option_variables(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
[
"--json",
'{"command": "variables", "thread_id": 1, "frame_index": 2}',
]
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
VariablesRequest(thread_id=1, frame_index=2)
)
@patch("cli.commands.break_cmd.resolve_path")
@patch("cli.cli.send_command")
async def test_break_command(
self, mock_send: Mock, mock_resolve: Mock
) -> None:
mock_send.return_value = 0
mock_resolve.return_value = "/path/to/fuchsia/src/main.rs"
exit_code = await main(["break", "src/main.rs:12"])
self.assertEqual(exit_code, 0)
mock_resolve.assert_called_once_with("src/main.rs")
mock_send.assert_called_once_with(
BreakRequest(file="/path/to/fuchsia/src/main.rs", line=12)
)
@patch("cli.commands.break_cmd.resolve_path")
@patch("cli.cli.send_command")
async def test_break_command_aliases(
self, mock_send: Mock, mock_resolve: Mock
) -> None:
mock_send.return_value = 0
mock_resolve.return_value = "/path/to/fuchsia/src/main.rs"
for alias in [
"breakpoint",
"b",
"setBreakpoints",
"set-breakpoints",
"set_breakpoints",
]:
mock_send.reset_mock()
mock_resolve.reset_mock()
exit_code = await main([alias, "src/main.rs:12"])
self.assertEqual(exit_code, 0, f"Failed for alias: {alias}")
mock_resolve.assert_called_once_with("src/main.rs")
mock_send.assert_called_once_with(
BreakRequest(file="/path/to/fuchsia/src/main.rs", line=12)
)
@patch("cli.cli.send_command")
async def test_json_option_break(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
[
"--json",
'{"command": "break", "file": "/path/to/file.rs", "line": 12}',
]
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
BreakRequest(file="/path/to/file.rs", line=12)
)
@patch("cli.commands.break_cmd.resolve_path")
async def test_break_command_invalid_format(
self, mock_resolve: Mock
) -> None:
# 1. No colon
exit_code = await main(["break", "src/main.rs"])
self.assertEqual(exit_code, 1)
mock_resolve.assert_not_called()
# 2. Non-integer line number
exit_code = await main(["break", "src/main.rs:abc"])
self.assertEqual(exit_code, 1)
mock_resolve.assert_not_called()
# 3. Negative line number
exit_code = await main(["break", "src/main.rs:-5"])
self.assertEqual(exit_code, 1)
mock_resolve.assert_not_called()
# 4. Could not resolve path
mock_resolve.return_value = None
exit_code = await main(["break", "src/missing.rs:10"])
self.assertEqual(exit_code, 1)
mock_resolve.assert_called_once_with("src/missing.rs")
@patch("os.path.isfile")
@patch.dict("os.environ", {"FUCHSIA_DIR": "/workspace/fuchsia"})
def test_resolve_path_fuchsia_dir(self, mock_isfile: Mock) -> None:
mock_isfile.side_effect = lambda p: p == "/workspace/fuchsia/src/foo.rs"
res = resolve_path("src/foo.rs")
self.assertEqual(res, "/workspace/fuchsia/src/foo.rs")
@patch("os.path.isfile")
@patch.dict("os.environ", {"FUCHSIA_DIR": "/workspace/fuchsia"})
def test_resolve_path_directory(self, mock_isfile: Mock) -> None:
mock_isfile.return_value = False
res = resolve_path("src")
self.assertIsNone(res)
@patch("os.path.isfile")
@patch.dict("os.environ", {"FUCHSIA_DIR": "/workspace/fuchsia"})
def test_resolve_path_unresolved(self, mock_isfile: Mock) -> None:
mock_isfile.return_value = False
res = resolve_path("missing/foo.rs")
self.assertIsNone(res)
@patch("cli.commands.break_cmd.resolve_path")
@patch("cli.cli.send_command")
async def test_break_command_delete(
self, mock_send: Mock, mock_resolve: Mock
) -> None:
mock_send.return_value = 0
mock_resolve.return_value = "/path/to/fuchsia/src/main.rs"
exit_code = await main(["break", "-d", "src/main.rs:12"])
self.assertEqual(exit_code, 0)
mock_resolve.assert_called_once_with("src/main.rs")
mock_send.assert_called_once_with(
BreakRequest(
file="/path/to/fuchsia/src/main.rs", line=12, delete=True
)
)
@patch("cli.cli.send_command")
async def test_evaluate_command(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(["evaluate", "--thread-id", "1", "x", "+", "y"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
EvaluateRequest(
thread_id=1,
frame_index=0,
expression="x + y",
start=0,
count=50,
)
)
@patch("cli.cli.send_command")
async def test_print_alias_command(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(["print", "--thread-id", "1", "x", "+", "y"])
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
EvaluateRequest(
thread_id=1,
frame_index=0,
expression="x + y",
start=0,
count=50,
)
)
@patch("cli.cli.send_command")
async def test_evaluate_command_options(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
[
"evaluate",
"--thread-id",
"1",
"x",
"--frame-index",
"2",
"--start",
"5",
"--count",
"10",
]
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
EvaluateRequest(
thread_id=1, frame_index=2, expression="x", start=5, count=10
)
)
@patch("cli.cli.send_command")
async def test_json_option_evaluate(self, mock_send: Mock) -> None:
mock_send.return_value = 0
exit_code = await main(
[
"--json",
'{"command": "evaluate", "thread_id": 1, "frame_index": 2, "expression": "x", "start": 5, "count": 10}',
]
)
self.assertEqual(exit_code, 0)
mock_send.assert_called_once_with(
EvaluateRequest(
thread_id=1, frame_index=2, expression="x", start=5, count=10
)
)
if __name__ == "__main__":
unittest.main()