Coverage for src/bluetooth_sig/gatt/characteristics/utils/debug_utils.py: 70%
80 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-30 00:10 +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.encode_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: int | 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 if isinstance(value, float):
47 formatted = f"{value:.{precision}f}"
48 else:
49 formatted = str(value)
51 if unit:
52 return f"{formatted} {unit}"
53 return formatted
55 @staticmethod
56 def format_hex_data(data: bytes | bytearray, separator: str = " ") -> str:
57 """Format binary data as hex string."""
58 return separator.join(f"{byte:02X}" for byte in data) # HEX_FORMAT_WIDTH = 2
60 @staticmethod
61 def format_binary_flags(value: int, bit_names: list[str]) -> str:
62 """Format integer as binary flags with names."""
63 flags: list[str] = []
64 for i, name in enumerate(bit_names):
65 if BitFieldUtils.test_bit(value, i):
66 flags.append(name)
67 return ", ".join(flags) if flags else "None"
69 @staticmethod
70 def validate_struct_format(data: bytes | bytearray, format_string: str) -> None:
71 """Validate data length matches struct format requirements."""
72 expected_size = struct.calcsize(format_string)
73 actual_size = len(data)
74 if actual_size != expected_size:
75 raise ValueError(f"Data size {actual_size} doesn't match format '{format_string}' size {expected_size}")
77 @staticmethod
78 def format_field_error(error: ParseFieldError, data: bytes | bytearray) -> str:
79 """Format a field-level parsing error with context.
81 Args:
82 error: The ParseFieldError to format
83 data: Complete raw data for context
85 Returns:
86 Formatted error message with hex dump and field context
88 """
89 parts = [f"Field '{error.field}' failed: {error.reason}"]
91 if error.offset is not None:
92 parts.append(f"Offset: {error.offset}")
94 # Show hex dump of the problematic slice or full data
95 if error.raw_slice:
96 hex_dump = DebugUtils.format_hex_data(error.raw_slice)
97 parts.append(f"Data: [{hex_dump}]")
98 elif data:
99 # Show context around the error
100 if error.offset is not None and error.offset < len(data):
101 # Show a few bytes before and after the offset
102 start = max(0, error.offset - 2)
103 end = min(len(data), error.offset + 3)
104 context_slice = data[start:end]
105 hex_dump = DebugUtils.format_hex_data(context_slice)
106 parts.append(f"Context at offset {start}: [{hex_dump}]")
107 else:
108 # Show all data if offset not available
109 hex_dump = DebugUtils.format_hex_data(data)
110 parts.append(f"Full data: [{hex_dump}]")
112 return " | ".join(parts)
114 @staticmethod
115 def format_field_errors(errors: list[Any], data: bytes | bytearray) -> str:
116 """Format multiple field errors into a readable message.
118 Args:
119 errors: List of ParseFieldError objects
120 data: Complete raw data for context
122 Returns:
123 Formatted string with all field errors and context
125 """
126 if not errors:
127 return "No field errors"
129 lines = [f"Found {len(errors)} field error(s):"]
130 for i, error in enumerate(errors, 1):
131 lines.append(f" {i}. {DebugUtils.format_field_error(error, data)}")
133 return "\n".join(lines)
135 @staticmethod
136 def format_parse_trace(trace: list[str]) -> str:
137 """Format parse trace as readable steps.
139 Args:
140 trace: List of parse trace entries
142 Returns:
143 Formatted trace string
145 """
146 if not trace:
147 return "No parse trace available"
149 lines = ["Parse trace:"]
150 for i, step in enumerate(trace, 1):
151 lines.append(f" {i}. {step}")
153 return "\n".join(lines)