Coverage for src / bluetooth_sig / gatt / characteristics / utils / debug_utils.py: 72%
78 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-18 11:17 +0000
1"""Debug utility methods for characteristic parsing."""
3from __future__ import annotations
5import struct
6from typing import Any
8from bluetooth_sig.types.data_types import ParseFieldError
9from bluetooth_sig.types.protocols import CharacteristicProtocol
11from .bit_field_utils import BitFieldUtils
14class DebugUtils:
15 """Utility class for debugging and testing support."""
17 # Format constants
18 HEX_FORMAT_WIDTH = 2
19 DEFAULT_PRECISION = 2
21 @staticmethod
22 def format_hex_dump(data: bytearray) -> str:
23 """Format data as a hex dump for debugging."""
24 return " ".join(f"{byte:02X}" for byte in data) # HEX_FORMAT_WIDTH = 2
26 @staticmethod
27 def validate_round_trip(characteristic: CharacteristicProtocol, original_data: bytearray) -> bool:
28 """Validate that parse/encode operations preserve data integrity."""
29 try:
30 parsed = characteristic.parse_value(original_data)
31 encoded = characteristic.build_value(parsed)
32 return bool(original_data == encoded)
33 except Exception: # pylint: disable=broad-except
34 return False
36 @staticmethod
37 def format_measurement_value(
38 value: float | str | None,
39 unit: str | None = None,
40 precision: int = 2, # DebugUtils.DEFAULT_PRECISION,
41 ) -> str:
42 """Format measurement value with unit for display."""
43 if value is None:
44 return "N/A"
46 formatted = f"{value:.{precision}f}" if isinstance(value, float) else str(value)
48 if unit:
49 return f"{formatted} {unit}"
50 return formatted
52 @staticmethod
53 def format_hex_data(data: bytes | bytearray, separator: str = " ") -> str:
54 """Format binary data as hex string."""
55 return separator.join(f"{byte:02X}" for byte in data) # HEX_FORMAT_WIDTH = 2
57 @staticmethod
58 def format_binary_flags(value: int, bit_names: list[str]) -> str:
59 """Format integer as binary flags with names."""
60 flags: list[str] = []
61 for i, name in enumerate(bit_names):
62 if BitFieldUtils.test_bit(value, i):
63 flags.append(name)
64 return ", ".join(flags) if flags else "None"
66 @staticmethod
67 def validate_struct_format(data: bytes | bytearray, format_string: str) -> None:
68 """Validate data length matches struct format requirements."""
69 expected_size = struct.calcsize(format_string)
70 actual_size = len(data)
71 if actual_size != expected_size:
72 raise ValueError(f"Data size {actual_size} doesn't match format '{format_string}' size {expected_size}")
74 @staticmethod
75 def format_field_error(error: ParseFieldError, data: bytes | bytearray) -> str:
76 """Format a field-level parsing error with context.
78 Args:
79 error: The ParseFieldError to format
80 data: Complete raw data for context
82 Returns:
83 Formatted error message with hex dump and field context
85 """
86 parts = [f"Field '{error.field}' failed: {error.reason}"]
88 if error.offset is not None:
89 parts.append(f"Offset: {error.offset}")
91 # Show hex dump of the problematic slice or full data
92 if error.raw_slice:
93 hex_dump = DebugUtils.format_hex_data(error.raw_slice)
94 parts.append(f"Data: [{hex_dump}]")
95 elif data:
96 # Show context around the error
97 if error.offset is not None and error.offset < len(data):
98 # Show a few bytes before and after the offset
99 start = max(0, error.offset - 2)
100 end = min(len(data), error.offset + 3)
101 context_slice = data[start:end]
102 hex_dump = DebugUtils.format_hex_data(context_slice)
103 parts.append(f"Context at offset {start}: [{hex_dump}]")
104 else:
105 # Show all data if offset not available
106 hex_dump = DebugUtils.format_hex_data(data)
107 parts.append(f"Full data: [{hex_dump}]")
109 return " | ".join(parts)
111 @staticmethod
112 def format_field_errors(errors: list[Any], data: bytes | bytearray) -> str:
113 """Format multiple field errors into a readable message.
115 Args:
116 errors: List of ParseFieldError objects
117 data: Complete raw data for context
119 Returns:
120 Formatted string with all field errors and context
122 """
123 if not errors:
124 return "No field errors"
126 lines = [f"Found {len(errors)} field error(s):"]
127 for i, error in enumerate(errors, 1):
128 lines.append(f" {i}. {DebugUtils.format_field_error(error, data)}")
130 return "\n".join(lines)
132 @staticmethod
133 def format_parse_trace(trace: list[str]) -> str:
134 """Format parse trace as readable steps.
136 Args:
137 trace: List of parse trace entries
139 Returns:
140 Formatted trace string
142 """
143 if not trace:
144 return "No parse trace available"
146 lines = ["Parse trace:"]
147 for i, step in enumerate(trace, 1):
148 lines.append(f" {i}. {step}")
150 return "\n".join(lines)