@@ -1615,6 +1615,175 @@ def test_payload_size_255(self):
16151615 assert result == struct .pack ("<HB" , 100 , 255 )
16161616
16171617
1618+ # ---------------------------------------------------------------------------
1619+ # Direct tests for _decode_temperature
1620+ # ---------------------------------------------------------------------------
1621+
1622+
1623+ class TestDecodeTemperature :
1624+ """Tests for _decode_temperature to kill no-test mutants."""
1625+
1626+ def test_decode_integer_temp (self ):
1627+ """22.0 => byte 22 + byte 0/10."""
1628+ raw = bytes ([22 , 0 ])
1629+ result = _protocol_module ._decode_temperature (raw , 0 )
1630+ assert result == 22.0
1631+
1632+ def test_decode_fractional_temp (self ):
1633+ """22.5 => byte 22 + byte 5/10."""
1634+ raw = bytes ([22 , 5 ])
1635+ result = _protocol_module ._decode_temperature (raw , 0 )
1636+ assert result == 22.5
1637+
1638+ def test_decode_uses_correct_offset (self ):
1639+ """Ensure offset is used correctly (not offset-1 or offset+2)."""
1640+ raw = bytes ([99 , 25 , 3 ])
1641+ result = _protocol_module ._decode_temperature (raw , 1 )
1642+ assert result == 25.3
1643+
1644+ def test_decode_addition_not_subtraction (self ):
1645+ """Verify integer + fraction, not integer - fraction."""
1646+ raw = bytes ([10 , 5 ])
1647+ result = _protocol_module ._decode_temperature (raw , 0 )
1648+ assert result == 10.5 # not 9.5
1649+
1650+ def test_decode_division_by_10 (self ):
1651+ """Verify division by 10, not multiplication or division by 11."""
1652+ raw = bytes ([20 , 9 ])
1653+ result = _protocol_module ._decode_temperature (raw , 0 )
1654+ assert result == 20.9 # 20 + 9/10.0
1655+
1656+
1657+ # ---------------------------------------------------------------------------
1658+ # Direct tests for _check_length
1659+ # ---------------------------------------------------------------------------
1660+
1661+
1662+ class TestCheckLength :
1663+ """Tests for _check_length to kill no-test mutants."""
1664+
1665+ def test_exact_length_does_not_raise (self ):
1666+ """Exact length should pass (< not <=)."""
1667+ _protocol_module ._check_length (bytes (4 ), 4 , "Test" )
1668+
1669+ def test_short_length_raises (self ):
1670+ """Shorter than expected should raise ProtocolError."""
1671+ with pytest .raises (ProtocolError , match = "Insufficient data for Test" ):
1672+ _protocol_module ._check_length (bytes (3 ), 4 , "Test" )
1673+
1674+ def test_longer_length_does_not_raise (self ):
1675+ """Longer than expected should pass."""
1676+ _protocol_module ._check_length (bytes (5 ), 4 , "Test" )
1677+
1678+ def test_error_message_contains_name (self ):
1679+ """Error message should include the parameter name."""
1680+ with pytest .raises (ProtocolError , match = "FlameEffect" ):
1681+ _protocol_module ._check_length (bytes (0 ), 23 , "FlameEffect" )
1682+
1683+ def test_error_message_contains_expected_bytes (self ):
1684+ """Error message should include expected byte count."""
1685+ with pytest .raises (ProtocolError , match = "expected 7 bytes" ):
1686+ _protocol_module ._check_length (bytes (3 ), 7 , "HeatSettings" )
1687+
1688+
1689+ # ---------------------------------------------------------------------------
1690+ # Tests to kill survived protocol mutants
1691+ # ---------------------------------------------------------------------------
1692+
1693+
1694+ class TestEncodeTemperatureModulus :
1695+ """Kill _encode_temperature__mutmut_6: temp % 1 vs temp % 2."""
1696+
1697+ def test_fractional_part_with_temp_above_1 (self ):
1698+ """For 22.5: fraction is 0.5, so second byte should be 5."""
1699+ result = _encode_temperature (22.5 )
1700+ assert result == bytes ([22 , 5 ])
1701+
1702+ def test_fractional_part_with_1_point_5 (self ):
1703+ """1.5 % 1 = 0.5 but 1.5 % 2 = 1.5 — kills the mutant."""
1704+ result = _encode_temperature (1.5 )
1705+ assert result == bytes ([1 , 5 ])
1706+
1707+
1708+ class TestDecodeTempUnitReturnsUnit :
1709+ """Kill _decode_temp_unit__mutmut_21: unit=None."""
1710+
1711+ def test_decoded_temp_unit_has_correct_value (self ):
1712+ """Verify decoded TempUnit carries the actual unit, not None."""
1713+ raw = _make_header (ParameterId .TEMPERATURE_UNIT , 1 ) + bytes ([1 ])
1714+ result = decode_parameter (ParameterId .TEMPERATURE_UNIT , raw )
1715+ assert isinstance (result , TempUnitParam )
1716+ assert result .unit == TempUnit .CELSIUS
1717+ assert result .unit is not None
1718+
1719+ def test_fahrenheit_temp_unit (self ):
1720+ raw = _make_header (ParameterId .TEMPERATURE_UNIT , 1 ) + bytes ([0 ])
1721+ result = decode_parameter (ParameterId .TEMPERATURE_UNIT , raw )
1722+ assert isinstance (result , TempUnitParam )
1723+ assert result .unit == TempUnit .FAHRENHEIT
1724+
1725+
1726+ class TestDecodeFlameEffectPulsating :
1727+ """Kill _decode_flame_effect__mutmut_26: & vs |."""
1728+
1729+ def test_pulsating_off_when_brightness_bit_only (self ):
1730+ """When brightness byte is 0b01, pulsating should be OFF.
1731+
1732+ With &1: (0b01 >> 1) & 1 = 0 & 1 = 0 -> OFF
1733+ With |1: (0b01 >> 1) | 1 = 0 | 1 = 1 -> ON (wrong!)
1734+ """
1735+ header = _make_header (ParameterId .FLAME_EFFECT , 20 )
1736+ payload = bytes (
1737+ [
1738+ 1 , # flame_effect (ON)
1739+ 2 , # flame_speed (wire 0-indexed) -> model 3
1740+ 0b01 , # brightness=HIGH, pulsating=OFF
1741+ 0 , # media_theme
1742+ 0 , # media_light
1743+ 0 ,
1744+ 0 ,
1745+ 0 ,
1746+ 0 , # media_color (RBGW)
1747+ 0 , # padding
1748+ 0 , # overhead_light
1749+ 0 ,
1750+ 0 ,
1751+ 0 ,
1752+ 0 , # overhead_color (RBGW)
1753+ 0 , # light_status
1754+ 0 , # flame_color
1755+ 0 ,
1756+ 0 , # padding
1757+ 0 , # ambient_sensor
1758+ ]
1759+ )
1760+ raw = header + payload
1761+ result = decode_parameter (ParameterId .FLAME_EFFECT , raw )
1762+ assert isinstance (result , FlameEffectParam )
1763+ assert result .pulsating_effect == PulsatingEffect .OFF
1764+
1765+
1766+ class TestEncodeHeatSettingsBoostFloor :
1767+ """Kill _encode_heat_settings__mutmut_19: max(0,...) vs max(1,...)."""
1768+
1769+ def test_boost_duration_1_encodes_to_zero_wire (self ):
1770+ """boost_duration=1 -> wire_boost = max(0, 1-1) = 0.
1771+
1772+ With max(1, 1-1) = max(1, 0) = 1 (wrong!)
1773+ """
1774+ param = HeatParam (
1775+ heat_status = HeatStatus .ON ,
1776+ heat_mode = HeatMode .BOOST ,
1777+ setpoint_temperature = 22.0 ,
1778+ boost_duration = 1 ,
1779+ )
1780+ b64 = encode_parameter (param )
1781+ raw = base64 .b64decode (b64 )
1782+ # Header(3) + [status, mode, temp_hi, temp_lo, boost]
1783+ wire_boost = raw [7 ] # offset 3+4 = 7
1784+ assert wire_boost == 0
1785+
1786+
16181787class TestEncodeParameterAsciiCasing :
16191788 """Document encode_parameter__mutmut_33 as equivalent mutant.
16201789
0 commit comments