blob: c4b8f978edbe2ef0556dea2dad4d4f50cf5b0321 [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 unittest
from typing import Any
from pydantic import ValidationError
from shared.protocol import (
PROTOCOL_VERSION,
deserialize_response,
make_request,
)
from shared.protocol.async_backtrace import (
AsyncBacktraceRequest,
AsyncBacktraceResponse,
)
from shared.protocol.attach import AttachRequest
from shared.protocol.break_request import BreakRequest
from shared.protocol.continue_request import ContinueRequest
from shared.protocol.detach import DetachRequest
from shared.protocol.evaluate import EvaluateRequest, EvaluateResponse
from shared.protocol.finish import FinishRequest
from shared.protocol.get_state import GetStateRequest, GetStateResponse
from shared.protocol.hello import HelloRequest
from shared.protocol.next_request import NextRequest
from shared.protocol.pause import PauseRequest
from shared.protocol.stack_trace import (
ProcessStackTraceResponse,
StackTraceRequest,
ThreadStackTraceResponse,
)
from shared.protocol.start import StartRequest
from shared.protocol.step_in import StepInRequest
from shared.protocol.stop import StopRequest
from shared.protocol.threads import ThreadsRequest
from shared.protocol.variables import VariablesRequest
from shared.protocol.wait_for_event import WaitForEventRequest
from zxdb_dap import AsyncTaskNode
class TestAsyncBacktraceRequestSchema(unittest.TestCase):
def test_valid_request_explicit_pid(self) -> None:
req = AsyncBacktraceRequest(pid=1234)
self.assertEqual(req.command, "async-backtrace")
self.assertEqual(req.pid, 1234)
def test_valid_request_no_pid(self) -> None:
req = AsyncBacktraceRequest()
self.assertEqual(req.command, "async-backtrace")
self.assertIsNone(req.pid)
class TestAsyncBacktraceResponseSchema(unittest.TestCase):
def test_response_serialization_with_tasks(self) -> None:
child = AsyncTaskNode(
id="task-2", name="child_task", file="bar.rs", line=20
)
parent = AsyncTaskNode(
id="task-1",
name="parent_task",
file="foo.rs",
line=10,
children=[child],
)
resp = AsyncBacktraceResponse(
process_id=1234,
tasks=[parent],
)
dumped = resp.model_dump(by_alias=True)
self.assertEqual(dumped["process_id"], 1234)
self.assertEqual(len(dumped["tasks"]), 1)
self.assertEqual(dumped["tasks"][0]["id"], "task-1")
self.assertEqual(dumped["tasks"][0]["name"], "parent_task")
self.assertEqual(len(dumped["tasks"][0]["children"]), 1)
self.assertEqual(dumped["tasks"][0]["children"][0]["id"], "task-2")
class TestStackTraceRequestSchema(unittest.TestCase):
def test_valid_request_thread_id(self) -> None:
req = StackTraceRequest(thread_id=1)
self.assertEqual(req.thread_id, 1)
self.assertIsNone(req.pid)
self.assertFalse(req.raw)
def test_valid_request_pid(self) -> None:
req = StackTraceRequest(pid=1234, raw=True)
self.assertIsNone(req.thread_id)
self.assertEqual(req.pid, 1234)
self.assertTrue(req.raw)
def test_malformed_request_both(self) -> None:
with self.assertRaises(ValidationError):
StackTraceRequest(thread_id=1, pid=1234)
def test_malformed_request_neither(self) -> None:
with self.assertRaises(ValidationError):
StackTraceRequest()
class TestDetachRequestSchema(unittest.TestCase):
def test_valid_request_pid(self) -> None:
req = DetachRequest(pid=1234)
self.assertEqual(req.pid, 1234)
self.assertFalse(req.all)
def test_valid_request_all(self) -> None:
req = DetachRequest(all=True)
self.assertIsNone(req.pid)
self.assertTrue(req.all)
def test_malformed_request_both(self) -> None:
with self.assertRaises(ValidationError):
DetachRequest(pid=1234, all=True)
def test_malformed_request_neither(self) -> None:
with self.assertRaises(ValidationError):
DetachRequest()
class TestHelloRequestSchema(unittest.TestCase):
def test_valid(self) -> None:
req = HelloRequest(version=PROTOCOL_VERSION)
self.assertEqual(req.version, PROTOCOL_VERSION)
def test_missing_version(self) -> None:
with self.assertRaises(ValidationError):
HelloRequest()
def test_type_coercion(self) -> None:
# Pydantic should coerce valid integer-like strings to integers by default
req = HelloRequest(version="5")
self.assertEqual(req.version, 5)
with self.assertRaises(ValidationError):
HelloRequest(version="not-an-int")
class TestAttachRequestSchema(unittest.TestCase):
def test_valid_int_pid(self) -> None:
req = AttachRequest(filter=1234)
self.assertEqual(req.filter, 1234)
def test_valid_string_name(self) -> None:
req = AttachRequest(filter="my_process")
self.assertEqual(req.filter, "my_process")
def test_missing_filter(self) -> None:
with self.assertRaises(ValidationError):
AttachRequest()
class TestWaitForEventRequestSchema(unittest.TestCase):
def test_valid(self) -> None:
req = WaitForEventRequest(last_seen_seq=10, timeout=5)
self.assertEqual(req.last_seen_seq, 10)
self.assertEqual(req.timeout, 5)
def test_optional_timeout(self) -> None:
req = WaitForEventRequest(last_seen_seq=10)
self.assertEqual(req.last_seen_seq, 10)
self.assertIsNone(req.timeout)
def test_missing_last_seen_seq(self) -> None:
with self.assertRaises(ValidationError):
WaitForEventRequest(timeout=5)
class TestPolymorphicParsing(unittest.TestCase):
def test_parse_start(self) -> None:
data = {"command": "start", "port": 15678, "connect": True}
req = make_request(data)
self.assertTrue(isinstance(req, StartRequest))
self.assertEqual(req.port, 15678)
self.assertTrue(req.connect)
def test_parse_stop(self) -> None:
data = {"command": "stop", "ack_seq": 10}
req = make_request(data)
self.assertTrue(isinstance(req, StopRequest))
self.assertEqual(req.ack_seq, 10)
def test_parse_finish(self) -> None:
data = {"command": "finish", "thread_id": 1, "single_thread": True}
req = make_request(data)
self.assertTrue(isinstance(req, FinishRequest))
self.assertEqual(req.thread_id, 1)
self.assertTrue(req.single_thread)
def test_parse_next(self) -> None:
data = {
"command": "next",
"thread_id": 1,
"single_thread": True,
"granularity": "line",
}
req = make_request(data)
self.assertTrue(isinstance(req, NextRequest))
self.assertEqual(req.thread_id, 1)
self.assertTrue(req.single_thread)
self.assertEqual(req.granularity, "line")
def test_parse_next_invalid_granularity(self) -> None:
data = {
"command": "next",
"thread_id": 1,
"granularity": "invalid_granularity",
}
with self.assertRaises(ValidationError):
make_request(data)
def test_parse_step_in(self) -> None:
data = {
"command": "step-in",
"thread_id": 1,
"single_thread": True,
"target_id": 0,
"granularity": "line",
}
req = make_request(data)
self.assertTrue(isinstance(req, StepInRequest))
self.assertEqual(req.thread_id, 1)
self.assertTrue(req.single_thread)
self.assertEqual(req.target_id, 0)
self.assertEqual(req.granularity, "line")
def test_parse_step_in_invalid_granularity(self) -> None:
data = {
"command": "step-in",
"thread_id": 1,
"granularity": "invalid_granularity",
}
with self.assertRaises(ValidationError):
make_request(data)
def test_parse_async_backtrace(self) -> None:
data = {"command": "async-backtrace", "pid": 1234}
req = make_request(data)
self.assertTrue(isinstance(req, AsyncBacktraceRequest))
self.assertEqual(req.pid, 1234)
def test_parse_unknown_command(self) -> None:
data = {"command": "unknown-cmd"}
with self.assertRaises(ValidationError):
make_request(data)
class TestResponseTypeAndDeserialization(unittest.TestCase):
def test_response_types_defined(self) -> None:
self.assertEqual(
AsyncBacktraceRequest.response_type, AsyncBacktraceResponse
)
self.assertEqual(GetStateRequest.response_type, GetStateResponse)
self.assertEqual(EvaluateRequest.response_type, EvaluateResponse)
self.assertEqual(
StackTraceRequest.response_type,
ThreadStackTraceResponse | ProcessStackTraceResponse,
)
self.assertEqual(AttachRequest.response_type, dict[str, Any])
self.assertEqual(BreakRequest.response_type, dict[str, Any])
self.assertEqual(ContinueRequest.response_type, dict[str, Any])
self.assertEqual(DetachRequest.response_type, dict[str, Any])
self.assertEqual(FinishRequest.response_type, dict[str, Any])
self.assertEqual(HelloRequest.response_type, dict[str, Any])
self.assertEqual(NextRequest.response_type, dict[str, Any])
self.assertEqual(PauseRequest.response_type, dict[str, Any])
self.assertEqual(StartRequest.response_type, dict[str, Any])
self.assertEqual(StepInRequest.response_type, dict[str, Any])
self.assertIsNone(StopRequest.response_type)
self.assertEqual(ThreadsRequest.response_type, dict[str, Any])
self.assertEqual(VariablesRequest.response_type, dict[str, Any])
self.assertIsNone(WaitForEventRequest.response_type)
def test_deserialize_async_backtrace_response(self) -> None:
req = AsyncBacktraceRequest(pid=1234)
json_line = (
'{"success": true, "message": null, "events": null, "body": '
'{"process_id": 1234, "tasks": []}}'
)
resp = deserialize_response(json_line, req)
self.assertTrue(resp.success)
self.assertIsInstance(resp.body, AsyncBacktraceResponse)
assert resp.body is not None
self.assertEqual(resp.body.process_id, 1234)
self.assertEqual(resp.body.tasks, [])
def test_deserialize_typed_response(self) -> None:
req = GetStateRequest()
json_line = (
'{"success": true, "message": null, "events": null, "body": '
'{"threads": [{"id": 1, "name": "t1"}], "processes": null, "breakpoints": null}}'
)
resp = deserialize_response(json_line, req)
self.assertTrue(resp.success)
self.assertIsInstance(resp.body, GetStateResponse)
assert resp.body is not None
self.assertEqual(len(resp.body.threads), 1)
self.assertEqual(resp.body.threads[0].name, "t1")
def test_deserialize_dict_response(self) -> None:
req = ThreadsRequest()
json_line = (
'{"success": true, "message": null, "events": null, "body": '
'{"threads": [{"id": 1, "name": "t1"}]}}'
)
resp = deserialize_response(json_line, req)
self.assertTrue(resp.success)
self.assertIsInstance(resp.body, dict)
self.assertEqual(resp.body, {"threads": [{"id": 1, "name": "t1"}]})
def test_deserialize_none_response(self) -> None:
req = StopRequest()
json_line = '{"success": true, "message": "stopped", "events": null, "body": null}'
resp = deserialize_response(json_line, req)
self.assertTrue(resp.success)
self.assertIsNone(resp.body)
self.assertEqual(resp.message, "stopped")
def test_deserialize_union_response_thread_and_process(self) -> None:
req = StackTraceRequest(thread_id=1)
thread_line = (
'{"success": true, "message": null, "events": null, "body": '
'{"thread_id": 1, "stack_frames": [{"frame_index": 0, "name": "foo", "line": 10, "column": 1}], "total_frames": 1}}'
)
resp_thread = deserialize_response(thread_line, req)
self.assertTrue(resp_thread.success)
self.assertIsInstance(resp_thread.body, ThreadStackTraceResponse)
process_line = (
'{"success": true, "message": null, "events": null, "body": '
'{"process_id": 1234, "stacks": [{"thread_id": 1, "stack_frames": [], "total_frames": 0}]}}'
)
resp_proc = deserialize_response(process_line, req)
self.assertTrue(resp_proc.success)
self.assertIsInstance(resp_proc.body, ProcessStackTraceResponse)
def test_deserialize_error_response_typed_request(self) -> None:
req = GetStateRequest()
json_line = '{"success": false, "message": "Handler failed", "events": null, "body": null}'
resp = deserialize_response(json_line, req)
self.assertFalse(resp.success)
self.assertIsNone(resp.body)
self.assertEqual(resp.message, "Handler failed")
def test_deserialize_evaluate_forbid_extra(self) -> None:
req = EvaluateRequest(thread_id=1, expression="x")
json_line = (
'{"success": true, "message": null, "events": null, "body": '
'{"result": "123", "type": "int", "unknown_field": 42}}'
)
with self.assertRaises(ValidationError):
deserialize_response(json_line, req)
if __name__ == "__main__":
unittest.main()