Files
id5100/test_civ.py
T

109 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Unit tests for the pure CI-V codec (civ.py). Stdlib only: `python3 -m unittest`."""
import unittest
import civ
class TestFreqCodec(unittest.TestCase):
# Real capture from the radio: VFO was 147.300000 MHz.
REAL_BCD = bytes.fromhex("0000304701")
REAL_HZ = 147_300_000
def test_bcd_to_freq_decodes_real_capture(self):
self.assertEqual(civ.bcd_to_freq(self.REAL_BCD), self.REAL_HZ)
def test_freq_to_bcd_matches_real_capture(self):
self.assertEqual(civ.freq_to_bcd(self.REAL_HZ), self.REAL_BCD)
def test_freq_roundtrip(self):
for hz in (146_520_000, 446_000_000, 147_300_000, 1_240_000_000):
self.assertEqual(civ.bcd_to_freq(civ.freq_to_bcd(hz)), hz)
class TestLevelCodec(unittest.TestCase):
# ICOM levels are 0..255 sent as 2-byte BCD "0000".."0255".
def test_level_to_bcd_known(self):
self.assertEqual(civ.level_to_bcd(0), b"\x00\x00")
self.assertEqual(civ.level_to_bcd(128), b"\x01\x28")
self.assertEqual(civ.level_to_bcd(255), b"\x02\x55")
def test_bcd_to_level_known(self):
self.assertEqual(civ.bcd_to_level(b"\x00\x00"), 0)
self.assertEqual(civ.bcd_to_level(b"\x01\x28"), 128)
self.assertEqual(civ.bcd_to_level(b"\x02\x55"), 255)
def test_level_rejects_out_of_range(self):
with self.assertRaises(ValueError):
civ.level_to_bcd(256)
with self.assertRaises(ValueError):
civ.level_to_bcd(-1)
class TestBuildFrame(unittest.TestCase):
def test_read_freq_frame(self):
# FE FE <radio=8C> <ctrl=E0> 03 FD
self.assertEqual(civ.build_frame(0x03), bytes.fromhex("fefe8ce003fd"))
def test_frame_with_data(self):
# set freq 147.3 MHz: FE FE 8C E0 05 <5 BCD> FD
frame = civ.build_frame(0x05, civ.freq_to_bcd(147_300_000))
self.assertEqual(frame, bytes.fromhex("fefe8ce005" + "0000304701" + "fd"))
class TestParseFrames(unittest.TestCase):
# Exact bytes the radio returned during the probe (echo + reply).
REAL_RX = bytes.fromhex("fefe8ce003fd" + "fefee08c030000304701fd")
def test_splits_echo_and_reply(self):
frames = civ.parse_frames(self.REAL_RX)
self.assertEqual(len(frames), 2)
echo, reply = frames
self.assertEqual((echo.to, echo.frm, echo.cn), (0x8C, 0xE0, 0x03))
self.assertEqual(echo.data, b"")
self.assertEqual((reply.to, reply.frm, reply.cn), (0xE0, 0x8C, 0x03))
self.assertEqual(reply.data, bytes.fromhex("0000304701"))
def test_ignores_leading_noise_and_partial_tail(self):
noisy = b"\x00\xff" + self.REAL_RX + b"\xfe\xfe\x99" # partial frame at end
frames = civ.parse_frames(noisy)
self.assertEqual(len(frames), 2)
def test_empty_buffer(self):
self.assertEqual(civ.parse_frames(b""), [])
class TestOffsetCodec(unittest.TestCase):
# PROVISIONAL: 3-byte little-endian BCD in units of 100 Hz.
# Round-trip validated until pinned to a real capture (plan Task 6).
def test_offset_roundtrip(self):
for hz in (600_000, 5_000_000, 1_600_000, 0):
self.assertEqual(civ.bcd_to_offset(civ.offset_to_bcd(hz)), hz)
def test_offset_is_three_bytes(self):
self.assertEqual(len(civ.offset_to_bcd(600_000)), 3)
def test_offset_rejects_negative(self):
with self.assertRaises(ValueError):
civ.offset_to_bcd(-1)
class TestToneCodec(unittest.TestCase):
# ICOM convention: CTCSS tone as 2-byte BCD of tone*10. 131.8 -> "13 18".
def test_tone_to_bcd_known(self):
self.assertEqual(civ.tone_to_bcd(88.5), b"\x08\x85")
self.assertEqual(civ.tone_to_bcd(131.8), b"\x13\x18")
self.assertEqual(civ.tone_to_bcd(100.0), b"\x10\x00")
def test_bcd_to_tone_known(self):
self.assertAlmostEqual(civ.bcd_to_tone(b"\x13\x18"), 131.8)
self.assertAlmostEqual(civ.bcd_to_tone(b"\x08\x85"), 88.5)
def test_tone_roundtrip(self):
for hz in (67.0, 88.5, 131.8, 146.2, 203.5):
self.assertAlmostEqual(civ.bcd_to_tone(civ.tone_to_bcd(hz)), hz)
if __name__ == "__main__":
unittest.main()