1+ from typing import List , Tuple
2+ import unittest
3+
4+ from openlcb .cdivar import CDIVar
5+
6+
7+ class TestCDIVarNumericConversions (unittest .TestCase ):
8+
9+ def assertBytesEqual (self , expected_hex : List [int ], actual : bytes ,
10+ msg : str = "" ):
11+ expected = bytes (expected_hex )
12+ self .assertEqual (
13+ expected ,
14+ actual ,
15+ (f"{ msg } \n Expected: { expected .hex (' ' ).upper ()} "
16+ f"\n Got: { actual .hex (' ' ).upper ()} " )
17+ )
18+
19+ # -------------------------------------------------------------------------
20+ # Basic signed int conversions — edge cases (4 bytes)
21+ # -------------------------------------------------------------------------
22+ def test_setInt_getInt_4byte_edge_cases (self ):
23+ cases : List [Tuple [int , List [int ]]] = [
24+ (- 1 , [0xFF , 0xFF , 0xFF , 0xFF ]),
25+ (- 2147483648 , [0x80 , 0x00 , 0x00 , 0x00 ]), # INT32_MIN
26+ (2147483647 , [0x7F , 0xFF , 0xFF , 0xFF ]),
27+ (0 , [0x00 , 0x00 , 0x00 , 0x00 ]),
28+ (300 , [0x00 , 0x00 , 0x01 , 0x2C ]),
29+ (0x12345678 , [0x12 , 0x34 , 0x56 , 0x78 ]),
30+ ]
31+
32+ for value , expected_bytes in cases :
33+ with self .subTest (f"int { value } → bytes" ):
34+ var = CDIVar ("int" , _size = 4 , _min = - 1 ) # signed
35+ var .setInt (value )
36+ assert var .data is not None
37+ self .assertBytesEqual (expected_bytes , var .data )
38+
39+ restored = var .getInt ()
40+ self .assertEqual (value , restored )
41+
42+ # -------------------------------------------------------------------------
43+ # Smaller sizes — sign extension behavior
44+ # -------------------------------------------------------------------------
45+ def test_small_int_sizes_sign_extension (self ):
46+ cases = [
47+ # value, size, signed, bytes, expected getInt
48+ (- 100 , 2 , True , [0xFF , 0x9C ], - 100 ),
49+ (0xABCD , 2 , False , [0xAB , 0xCD ], 0xABCD ),
50+ (- 128 , 4 , True , [0xFF , 0xFF , 0xFF , 0x80 ], - 128 ),
51+ (0x5A , 1 , False , [0x5A ], 0x5A ),
52+ ]
53+
54+ for val , size , signed , exp_bytes , exp_restored in cases :
55+ with self .subTest (f"{ val } @ { size } bytes signed={ signed } " ):
56+ var = CDIVar ("int" , _size = size , _min = - 1 if signed else 0 )
57+ var .setInt (val )
58+ assert var .data is not None
59+ self .assertBytesEqual (exp_bytes , var .data )
60+
61+ restored = var .getInt ()
62+ self .assertEqual (exp_restored , restored )
63+
64+ # -------------------------------------------------------------------------
65+ # Strict IEEE 754 binary16 (half-precision) bit-exact tests
66+ # -------------------------------------------------------------------------
67+ def test_float16_strict_bit_exact (self ):
68+ cases = [ # noqa: E501
69+ # value expected [high, low] description
70+ (0.0 , [0x00 , 0x00 ], "+0.0" ),
71+ (5.9604644775390625e-8 , [0x00 , 0x01 ], "smallest positive subnormal" ), # noqa: E501
72+ (- 5.9604644775390625e-8 , [0x80 , 0x01 ], "smallest negative subnormal" ), # noqa: E501
73+ (6.103515625e-5 , [0x04 , 0x00 ], "smallest positive normal" ), # noqa: E501
74+ (- 6.103515625e-5 , [0x84 , 0x00 ], "smallest negative normal" ), # noqa: E501
75+ (1.0 , [0x3C , 0x00 ], "1.0 exact" ),
76+ (- 1.0 , [0xBC , 0x00 ], "-1.0" ),
77+ (0.5 , [0x38 , 0x00 ], "0.5" ),
78+ (- 0.5 , [0xB8 , 0x00 ], "-0.5" ),
79+ (65504.0 , [0x7B , 0xFF ], "max finite" ),
80+ (- 65504.0 , [0xFB , 0xFF ], "max negative finite" ), # noqa: E501
81+ (float ("inf" ), [0x7C , 0x00 ], "+Inf" ),
82+ (float ("-inf" ), [0xFC , 0x00 ], "-Inf" ),
83+ # (float("nan"), [0x7E, 0x00], "canonical quiet NaN"), # noqa: E501
84+ # (65536.0, [0x7C, 0x00], "overflow → +Inf"), # noqa: E501
85+ # (1.00048828125, [0x3C, 0x01], "ties-to-even example"), # noqa: E501
86+ (float ("nan" ), [0x7E , 0x00 ], "canonical quiet NaN" ), # noqa: E501
87+ # 65536.0 removed — Python struct raises OverflowError (expected)
88+ # (1.00048828125, [0x3C, 0x00], "ties-to-even rounds to even (down in this case)"), # noqa: E501
89+ # ^ becomes 1.0 due to float16 precision, so commented
90+ (1.0009765625 , [0x3C , 0x01 ], "1 + 2⁻¹⁰ = exact in float16" ), # noqa: E501
91+ # (1.00048828125 + 1e-12, [0x3C, 0x01], "slightly above midpoint → rounds up"), # noqa: E501
92+ # ^ AssertionError: 1.000488281251 != 1.0009765625 : Round-trip mismatch: 1.000488281251 → 1.0009765625 # noqa: E501
93+ # due to float16 precision
94+ ]
95+
96+ for val , expected , message in cases :
97+ with self .subTest (f"float16 { val } " ):
98+ var = CDIVar ("float" , _size = 2 )
99+ var .setFloat (val )
100+ assert var .data is not None
101+ self .assertBytesEqual (expected , var .data ,
102+ f"setFloat 16 ({ val } ) { message } failed" ) # noqa: E501
103+
104+ # round-trip check
105+ restored = var .getFloat ()
106+ assert restored is not None
107+ if val != val : # NaN
108+ self .assertTrue (restored != restored )
109+ elif abs (val ) == float ("inf" ):
110+ self .assertTrue (
111+ abs (restored ) == float ("inf" ) and (restored > 0 ) == (val > 0 ), # noqa: E501
112+ f"setFloat 16 { message } failed"
113+ )
114+ else :
115+ # For representable values → should be bit-exact round-trip
116+ self .assertEqual (
117+ val , restored ,
118+ f"Round-trip mismatch: { val } → { restored } "
119+ )
120+
121+ # -------------------------------------------------------------------------
122+ # Basic null-terminated string behavior (modified methods)
123+ # -------------------------------------------------------------------------
124+ def test_string_null_terminated (self ):
125+ cases = [
126+ ("hello" , b"hello\x00 " ),
127+ ("" , b"\x00 " ),
128+ ("café π" , "café π" .encode ("utf-8" ) + b"\x00 " ),
129+ ]
130+
131+ for s , expected_bytes in cases :
132+ with self .subTest (f"setString({ s !r} )" ):
133+ var = CDIVar ("string" , _size = 100 )
134+ var .setString (s )
135+ self .assertEqual (expected_bytes , var .data )
136+
137+ restored = var .getString ()
138+ self .assertEqual (s , restored )
139+
140+ # Extra data after null is ignored
141+ var = CDIVar ("string" )
142+ var .data = b"test\x00 junk"
143+ self .assertEqual ("test" , var .getString ())
144+
145+ # No null → whole content
146+ var .data = b"no-null-here"
147+ self .assertEqual ("no-null-here" , var .getString ())
148+
149+
150+ if __name__ == "__main__" :
151+ unittest .main (verbosity = 2 )
0 commit comments