From e99416456afd4aa8bde42016826f9a345291cbf3 Mon Sep 17 00:00:00 2001 From: Matthew Poletiek Date: Tue, 8 Dec 2020 21:03:16 -0600 Subject: Initial Commit --- tests/__init__.py | 151 +++ tests/icom_clone_simulator.py | 195 ++++ tests/images/Alinco_DJ-G7EG.img | Bin 0 -> 108480 bytes tests/images/Alinco_DJ175.img | Bin 0 -> 6896 bytes tests/images/Alinco_DJ596.img | Bin 0 -> 4096 bytes tests/images/Alinco_DR235T.img | Bin 0 -> 4096 bytes tests/images/AnyTone_OBLTR-8R.img | Bin 0 -> 32768 bytes tests/images/AnyTone_TERMN-8R.img | Bin 0 -> 32768 bytes tests/images/BTECH_GMRS-50X1.img | Bin 0 -> 16384 bytes tests/images/BTECH_GMRS-V1.img | Bin 0 -> 8200 bytes tests/images/BTECH_MURS-V1.img | Bin 0 -> 8200 bytes tests/images/BTECH_UV-2501+220.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-25X2.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-25X4.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-5001.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-50X2.img | Bin 0 -> 16384 bytes tests/images/BTECH_UV-50X3.img | Bin 0 -> 32768 bytes tests/images/BTECH_UV-5X3.img | Bin 0 -> 8206 bytes tests/images/Baofeng_BF-888.img | Bin 0 -> 992 bytes tests/images/Baofeng_BF-A58S.img | Bin 0 -> 8361 bytes tests/images/Baofeng_BF-T1.img | Bin 0 -> 2048 bytes tests/images/Baofeng_F-11.img | Bin 0 -> 6472 bytes tests/images/Baofeng_UV-3R.img | Bin 0 -> 3648 bytes tests/images/Baofeng_UV-5R.img | Bin 0 -> 6472 bytes tests/images/Baofeng_UV-6R.img | Bin 0 -> 8200 bytes tests/images/Baofeng_UV-B5.img | Bin 0 -> 4144 bytes tests/images/Baojie_BJ-9900.img | Bin 0 -> 6385 bytes tests/images/Boblov_X3Plus.img | Bin 0 -> 1008 bytes tests/images/Feidaxin_FD-268A.img | Bin 0 -> 2048 bytes tests/images/Feidaxin_FD-268B.img | Bin 0 -> 2048 bytes tests/images/Feidaxin_FD-288B.img | Bin 0 -> 2048 bytes tests/images/Generic_CSV.csv | 104 +++ tests/images/Icom_IC-208H.img | Bin 0 -> 9728 bytes tests/images/Icom_IC-2100H.img | Bin 0 -> 2016 bytes tests/images/Icom_IC-2200H.img | Bin 0 -> 6848 bytes tests/images/Icom_IC-2300H.img | Bin 0 -> 6304 bytes tests/images/Icom_IC-2720H.img | Bin 0 -> 5152 bytes tests/images/Icom_IC-2730A.img | Bin 0 -> 21312 bytes tests/images/Icom_IC-2820H.img | Bin 0 -> 44224 bytes tests/images/Icom_IC-P7.img | Bin 0 -> 29952 bytes tests/images/Icom_IC-Q7A.img | Bin 0 -> 1984 bytes tests/images/Icom_IC-T70.img | Bin 0 -> 6624 bytes tests/images/Icom_IC-T7H.img | Bin 0 -> 944 bytes tests/images/Icom_IC-T8A.img | Bin 0 -> 1968 bytes tests/images/Icom_IC-V82_U82.img | Bin 0 -> 6464 bytes tests/images/Icom_IC-W32A.img | Bin 0 -> 4064 bytes tests/images/Icom_IC-W32E.img | Bin 0 -> 4065 bytes tests/images/Icom_ID-31A.img | Bin 0 -> 87296 bytes tests/images/Icom_ID-51.img | Bin 0 -> 129856 bytes tests/images/Icom_ID-51_Plus.img | Bin 0 -> 129856 bytes tests/images/Icom_ID-800H_v2.img | Bin 0 -> 14528 bytes tests/images/Icom_ID-880H.img | Bin 0 -> 62976 bytes tests/images/Jetstream_JT220M.img | Bin 0 -> 8192 bytes tests/images/Jetstream_JT270M.img | Bin 0 -> 8192 bytes tests/images/Jetstream_JT270MH.img | Bin 0 -> 8192 bytes tests/images/KYD_IP-620.img | Bin 0 -> 8192 bytes tests/images/KYD_NC-630A.img | Bin 0 -> 824 bytes tests/images/Kenwood_HMK.hmk | 71 ++ tests/images/Kenwood_TH-D72_clone_mode.img | Bin 0 -> 65536 bytes tests/images/Kenwood_TK-272G.img | Bin 0 -> 32768 bytes tests/images/Kenwood_TK-3180K2.img | Bin 0 -> 53681 bytes tests/images/Kenwood_TK-760G.img | Bin 0 -> 32768 bytes tests/images/Kenwood_TK-8102.img | Bin 0 -> 1040 bytes tests/images/Kenwood_TK-8180.img | Bin 0 -> 53673 bytes tests/images/Kenwood_TS-480_CloneMode.img | Bin 0 -> 3018 bytes tests/images/LUITON_LT-725UV.img | Bin 0 -> 7176 bytes tests/images/Leixen_VV-898.img | Bin 0 -> 8192 bytes tests/images/Leixen_VV-898S.img | Bin 0 -> 8192 bytes tests/images/Polmar_DB-50M.img | Bin 0 -> 32768 bytes tests/images/Puxing_PX-2R.img | Bin 0 -> 4064 bytes tests/images/Puxing_PX-777.img | Bin 0 -> 3168 bytes tests/images/Puxing_PX-888K.img | Bin 0 -> 4096 bytes tests/images/QYT_KT7900D.img | Bin 0 -> 16384 bytes tests/images/QYT_KT8900D.img | Bin 0 -> 16384 bytes tests/images/Radioddity_R2.img | Bin 0 -> 1181 bytes tests/images/Radtel_T18.img | Bin 0 -> 1008 bytes tests/images/Retevis_RT21.img | Bin 0 -> 1024 bytes tests/images/Retevis_RT22.img | Bin 0 -> 1032 bytes tests/images/Retevis_RT23.img | Bin 0 -> 4096 bytes tests/images/Retevis_RT26.img | Bin 0 -> 1024 bytes tests/images/TDXone_TD-Q8A.img | Bin 0 -> 8200 bytes tests/images/TYT_TH-350.img | Bin 0 -> 4297 bytes tests/images/TYT_TH-7800.img | Bin 0 -> 65296 bytes tests/images/TYT_TH-9800.img | Bin 0 -> 65296 bytes tests/images/TYT_TH-UV3R-25.img | Bin 0 -> 2864 bytes tests/images/TYT_TH-UV3R.img | Bin 0 -> 2320 bytes tests/images/TYT_TH-UV8000.img | Bin 0 -> 5040 bytes tests/images/TYT_TH-UVF1.img | Bin 0 -> 4112 bytes tests/images/TYT_TH9000_144.img | Bin 0 -> 16384 bytes tests/images/Vertex_Standard_VXA-700.img | Bin 0 -> 4096 bytes tests/images/WACCOM_MINI-8900.img | Bin 0 -> 16384 bytes tests/images/Wouxun_KG-816.img | Bin 0 -> 8192 bytes tests/images/Wouxun_KG-818.img | Bin 0 -> 8192 bytes tests/images/Wouxun_KG-UV6.img | Bin 0 -> 8192 bytes tests/images/Wouxun_KG-UV8D.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UV8D_Plus.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UV8E.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UV9D_Plus.img | Bin 0 -> 32768 bytes tests/images/Wouxun_KG-UVD1P.img | Bin 0 -> 8192 bytes tests/images/Yaesu_FT-1500M.img | Bin 0 -> 4140 bytes tests/images/Yaesu_FT-1802M.img | Bin 0 -> 8011 bytes tests/images/Yaesu_FT-1D_R.img | Bin 0 -> 130507 bytes tests/images/Yaesu_FT-25R.img | Bin 0 -> 8689 bytes tests/images/Yaesu_FT-2800M.img | Bin 0 -> 7680 bytes tests/images/Yaesu_FT-2900R_1900R.img | Bin 0 -> 8000 bytes tests/images/Yaesu_FT-450D.img | Bin 0 -> 15024 bytes tests/images/Yaesu_FT-4VR.img | Bin 0 -> 8697 bytes tests/images/Yaesu_FT-4XE.img | Bin 0 -> 8697 bytes tests/images/Yaesu_FT-4XR.img | Bin 0 -> 8689 bytes tests/images/Yaesu_FT-50.img | Bin 0 -> 3723 bytes tests/images/Yaesu_FT-60.img | Bin 0 -> 28617 bytes tests/images/Yaesu_FT-65E.img | Bin 0 -> 8697 bytes tests/images/Yaesu_FT-65R.img | Bin 0 -> 8693 bytes tests/images/Yaesu_FT-70D.img | Bin 0 -> 65227 bytes tests/images/Yaesu_FT-7100M.img | Bin 0 -> 7936 bytes tests/images/Yaesu_FT-7800_7900.img | Bin 0 -> 31561 bytes tests/images/Yaesu_FT-817.img | Bin 0 -> 6509 bytes tests/images/Yaesu_FT-817ND.img | Bin 0 -> 6521 bytes tests/images/Yaesu_FT-817ND_US.img | Bin 0 -> 6651 bytes tests/images/Yaesu_FT-818.img | Bin 0 -> 6730 bytes tests/images/Yaesu_FT-857_897.img | Bin 0 -> 7341 bytes tests/images/Yaesu_FT-857_897_US.img | Bin 0 -> 7481 bytes tests/images/Yaesu_FT-8800.img | Bin 0 -> 22217 bytes tests/images/Yaesu_FT-8900.img | Bin 0 -> 14793 bytes tests/images/Yaesu_FT2D_R.img | Bin 0 -> 130507 bytes tests/images/Yaesu_FT3D_R.img | Bin 0 -> 130652 bytes tests/images/Yaesu_FTM-3200D_R.img | Bin 0 -> 65227 bytes tests/images/Yaesu_FTM-350.img | Bin 0 -> 65664 bytes tests/images/Yaesu_VX-2.img | Bin 0 -> 32595 bytes tests/images/Yaesu_VX-3.img | Bin 0 -> 32587 bytes tests/images/Yaesu_VX-5.img | Bin 0 -> 8123 bytes tests/images/Yaesu_VX-6.img | Bin 0 -> 32587 bytes tests/images/Yaesu_VX-7.img | Bin 0 -> 16211 bytes tests/images/Yaesu_VX-8DR.img | Bin 0 -> 65227 bytes tests/images/Yaesu_VX-8GE.img | Bin 0 -> 65227 bytes tests/images/Yaesu_VX-8R.img | Bin 0 -> 65227 bytes tests/run_tests | 3 + tests/run_tests.py | 1372 ++++++++++++++++++++++++++++ tests/test_drivers.py | 24 + tests/unit/__init__.py | 0 tests/unit/base.py | 50 + tests/unit/test_bitwise.py | 341 +++++++ tests/unit/test_chirp_common.py | 415 +++++++++ tests/unit/test_directory.py | 69 ++ tests/unit/test_icom_clone.py | 117 +++ tests/unit/test_import_logic.py | 373 ++++++++ tests/unit/test_mappingmodel.py | 283 ++++++ tests/unit/test_memedit_edits.py | 82 ++ tests/unit/test_platform.py | 62 ++ tests/unit/test_repeaterbook.py | 28 + tests/unit/test_settings.py | 140 +++ tests/unit/test_shiftdialog.py | 112 +++ tests/unit/test_utils.py | 21 + tests/unit/test_yaesu_clone.py | 41 + 154 files changed, 4054 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/icom_clone_simulator.py create mode 100644 tests/images/Alinco_DJ-G7EG.img create mode 100644 tests/images/Alinco_DJ175.img create mode 100644 tests/images/Alinco_DJ596.img create mode 100644 tests/images/Alinco_DR235T.img create mode 100644 tests/images/AnyTone_OBLTR-8R.img create mode 100644 tests/images/AnyTone_TERMN-8R.img create mode 100644 tests/images/BTECH_GMRS-50X1.img create mode 100644 tests/images/BTECH_GMRS-V1.img create mode 100644 tests/images/BTECH_MURS-V1.img create mode 100755 tests/images/BTECH_UV-2501+220.img create mode 100755 tests/images/BTECH_UV-25X2.img create mode 100755 tests/images/BTECH_UV-25X4.img create mode 100755 tests/images/BTECH_UV-5001.img create mode 100755 tests/images/BTECH_UV-50X2.img create mode 100644 tests/images/BTECH_UV-50X3.img create mode 100755 tests/images/BTECH_UV-5X3.img create mode 100644 tests/images/Baofeng_BF-888.img create mode 100644 tests/images/Baofeng_BF-A58S.img create mode 100644 tests/images/Baofeng_BF-T1.img create mode 100644 tests/images/Baofeng_F-11.img create mode 100644 tests/images/Baofeng_UV-3R.img create mode 100644 tests/images/Baofeng_UV-5R.img create mode 100755 tests/images/Baofeng_UV-6R.img create mode 100644 tests/images/Baofeng_UV-B5.img create mode 100644 tests/images/Baojie_BJ-9900.img create mode 100644 tests/images/Boblov_X3Plus.img create mode 100755 tests/images/Feidaxin_FD-268A.img create mode 100755 tests/images/Feidaxin_FD-268B.img create mode 100755 tests/images/Feidaxin_FD-288B.img create mode 100644 tests/images/Generic_CSV.csv create mode 100644 tests/images/Icom_IC-208H.img create mode 100644 tests/images/Icom_IC-2100H.img create mode 100644 tests/images/Icom_IC-2200H.img create mode 100644 tests/images/Icom_IC-2300H.img create mode 100644 tests/images/Icom_IC-2720H.img create mode 100644 tests/images/Icom_IC-2730A.img create mode 100644 tests/images/Icom_IC-2820H.img create mode 100755 tests/images/Icom_IC-P7.img create mode 100644 tests/images/Icom_IC-Q7A.img create mode 100644 tests/images/Icom_IC-T70.img create mode 100644 tests/images/Icom_IC-T7H.img create mode 100644 tests/images/Icom_IC-T8A.img create mode 100644 tests/images/Icom_IC-V82_U82.img create mode 100644 tests/images/Icom_IC-W32A.img create mode 100644 tests/images/Icom_IC-W32E.img create mode 100644 tests/images/Icom_ID-31A.img create mode 100644 tests/images/Icom_ID-51.img create mode 100755 tests/images/Icom_ID-51_Plus.img create mode 100644 tests/images/Icom_ID-800H_v2.img create mode 100644 tests/images/Icom_ID-880H.img create mode 100644 tests/images/Jetstream_JT220M.img create mode 100644 tests/images/Jetstream_JT270M.img create mode 100644 tests/images/Jetstream_JT270MH.img create mode 100755 tests/images/KYD_IP-620.img create mode 100755 tests/images/KYD_NC-630A.img create mode 100644 tests/images/Kenwood_HMK.hmk create mode 100644 tests/images/Kenwood_TH-D72_clone_mode.img create mode 100755 tests/images/Kenwood_TK-272G.img create mode 100644 tests/images/Kenwood_TK-3180K2.img create mode 100755 tests/images/Kenwood_TK-760G.img create mode 100644 tests/images/Kenwood_TK-8102.img create mode 100644 tests/images/Kenwood_TK-8180.img create mode 100644 tests/images/Kenwood_TS-480_CloneMode.img create mode 100755 tests/images/LUITON_LT-725UV.img create mode 100644 tests/images/Leixen_VV-898.img create mode 100755 tests/images/Leixen_VV-898S.img create mode 100644 tests/images/Polmar_DB-50M.img create mode 100644 tests/images/Puxing_PX-2R.img create mode 100644 tests/images/Puxing_PX-777.img create mode 100644 tests/images/Puxing_PX-888K.img create mode 100755 tests/images/QYT_KT7900D.img create mode 100755 tests/images/QYT_KT8900D.img create mode 100644 tests/images/Radioddity_R2.img create mode 100755 tests/images/Radtel_T18.img create mode 100644 tests/images/Retevis_RT21.img create mode 100644 tests/images/Retevis_RT22.img create mode 100755 tests/images/Retevis_RT23.img create mode 100644 tests/images/Retevis_RT26.img create mode 100755 tests/images/TDXone_TD-Q8A.img create mode 100644 tests/images/TYT_TH-350.img create mode 100644 tests/images/TYT_TH-7800.img create mode 100644 tests/images/TYT_TH-9800.img create mode 100755 tests/images/TYT_TH-UV3R-25.img create mode 100644 tests/images/TYT_TH-UV3R.img create mode 100644 tests/images/TYT_TH-UV8000.img create mode 100644 tests/images/TYT_TH-UVF1.img create mode 100644 tests/images/TYT_TH9000_144.img create mode 100644 tests/images/Vertex_Standard_VXA-700.img create mode 100755 tests/images/WACCOM_MINI-8900.img create mode 100644 tests/images/Wouxun_KG-816.img create mode 100644 tests/images/Wouxun_KG-818.img create mode 100644 tests/images/Wouxun_KG-UV6.img create mode 100644 tests/images/Wouxun_KG-UV8D.img create mode 100644 tests/images/Wouxun_KG-UV8D_Plus.img create mode 100644 tests/images/Wouxun_KG-UV8E.img create mode 100644 tests/images/Wouxun_KG-UV9D_Plus.img create mode 100644 tests/images/Wouxun_KG-UVD1P.img create mode 100644 tests/images/Yaesu_FT-1500M.img create mode 100644 tests/images/Yaesu_FT-1802M.img create mode 100644 tests/images/Yaesu_FT-1D_R.img create mode 100644 tests/images/Yaesu_FT-25R.img create mode 100644 tests/images/Yaesu_FT-2800M.img create mode 100755 tests/images/Yaesu_FT-2900R_1900R.img create mode 100644 tests/images/Yaesu_FT-450D.img create mode 100644 tests/images/Yaesu_FT-4VR.img create mode 100644 tests/images/Yaesu_FT-4XE.img create mode 100644 tests/images/Yaesu_FT-4XR.img create mode 100755 tests/images/Yaesu_FT-50.img create mode 100644 tests/images/Yaesu_FT-60.img create mode 100644 tests/images/Yaesu_FT-65E.img create mode 100644 tests/images/Yaesu_FT-65R.img create mode 100644 tests/images/Yaesu_FT-70D.img create mode 100644 tests/images/Yaesu_FT-7100M.img create mode 100644 tests/images/Yaesu_FT-7800_7900.img create mode 100644 tests/images/Yaesu_FT-817.img create mode 100644 tests/images/Yaesu_FT-817ND.img create mode 100644 tests/images/Yaesu_FT-817ND_US.img create mode 100644 tests/images/Yaesu_FT-818.img create mode 100644 tests/images/Yaesu_FT-857_897.img create mode 100644 tests/images/Yaesu_FT-857_897_US.img create mode 100644 tests/images/Yaesu_FT-8800.img create mode 100644 tests/images/Yaesu_FT-8900.img create mode 100644 tests/images/Yaesu_FT2D_R.img create mode 100644 tests/images/Yaesu_FT3D_R.img create mode 100644 tests/images/Yaesu_FTM-3200D_R.img create mode 100644 tests/images/Yaesu_FTM-350.img create mode 100644 tests/images/Yaesu_VX-2.img create mode 100644 tests/images/Yaesu_VX-3.img create mode 100644 tests/images/Yaesu_VX-5.img create mode 100644 tests/images/Yaesu_VX-6.img create mode 100644 tests/images/Yaesu_VX-7.img create mode 100644 tests/images/Yaesu_VX-8DR.img create mode 100644 tests/images/Yaesu_VX-8GE.img create mode 100644 tests/images/Yaesu_VX-8R.img create mode 100755 tests/run_tests create mode 100755 tests/run_tests.py create mode 100644 tests/test_drivers.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/base.py create mode 100644 tests/unit/test_bitwise.py create mode 100644 tests/unit/test_chirp_common.py create mode 100644 tests/unit/test_directory.py create mode 100644 tests/unit/test_icom_clone.py create mode 100644 tests/unit/test_import_logic.py create mode 100644 tests/unit/test_mappingmodel.py create mode 100644 tests/unit/test_memedit_edits.py create mode 100644 tests/unit/test_platform.py create mode 100644 tests/unit/test_repeaterbook.py create mode 100644 tests/unit/test_settings.py create mode 100644 tests/unit/test_shiftdialog.py create mode 100644 tests/unit/test_utils.py create mode 100644 tests/unit/test_yaesu_clone.py (limited to 'tests') diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d04e516 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,151 @@ +import glob +import logging +import os +import re +import shutil +import sys +import tempfile +import unittest + +import six + +from chirp import directory + +from tests import run_tests + + +LOG = logging.getLogger('testadapter') + + +class TestAdapterMeta(type): + def __new__(cls, name, parents, dct): + return super(TestAdapterMeta, cls).__new__(cls, name, parents, dct) + + +class TestAdapter(unittest.TestCase): + RADIO_CLASS = None + SOURCE_IMAGE = None + RADIO_INST = None + testwrapper = None + + def shortDescription(self): + test = self.id().split('.')[-1].replace('test_', '').replace('_', ' ') + return 'Testing %s %s' % (self.RADIO_CLASS.get_name(), test) + + @classmethod + def setUpClass(cls): + if not cls.testwrapper: + # Initialize the radio once per class invocation to save + # bitwise parse time + # Do this for things like Generic_CSV, that demand it + _base, ext = os.path.splitext(cls.SOURCE_IMAGE) + cls.testimage = tempfile.mktemp(ext) + shutil.copy(cls.SOURCE_IMAGE, cls.testimage) + cls.testwrapper = run_tests.TestWrapper(cls.RADIO_CLASS, + cls.testimage) + + @classmethod + def tearDownClass(cls): + os.remove(cls.testimage) + + def _runtest(self, test): + tw = run_tests.TestWrapper(self.RADIO_CLASS, + self.testimage, + dst=self.RADIO_INST) + testcase = test(tw) + testcase.prepare() + try: + failures = testcase.run() + if failures: + raise failures[0] + except run_tests.TestCrashError as e: + raise e.get_original_exception() + except run_tests.TestSkippedError as e: + raise unittest.SkipTest(str(e)) + finally: + testcase.cleanup() + + def test_copy_all(self): + self._runtest(run_tests.TestCaseCopyAll) + + def test_brute_force(self): + self._runtest(run_tests.TestCaseBruteForce) + + def test_edges(self): + self._runtest(run_tests.TestCaseEdges) + + def test_settings(self): + self._runtest(run_tests.TestCaseSettings) + + def test_banks(self): + self._runtest(run_tests.TestCaseBanks) + + def test_detect(self): + self._runtest(run_tests.TestCaseDetect) + + def test_clone(self): + self._runtest(run_tests.TestCaseClone) + + +def _get_sub_devices(rclass, testimage): + try: + tw = run_tests.TestWrapper(rclass, None) + except Exception as e: + tw = run_tests.TestWrapper(rclass, testimage) + + rf = tw.do("get_features") + if rf.has_sub_devices: + return tw.do("get_sub_devices") + else: + return [rclass] + + +class RadioSkipper(unittest.TestCase): + def test_is_supported_by_environment(self): + raise unittest.SkipTest('Running in py3 and driver is not supported') + + +def load_tests(loader, tests, pattern, suite=None): + if not suite: + suite = unittest.TestSuite() + + base = os.path.dirname(os.path.abspath(__file__)) + base = os.path.join(base, 'images') + images = glob.glob(os.path.join(base, "*")) + tests = {img: os.path.splitext(os.path.basename(img))[0] for img in images} + + if pattern == 'test*.py': + # This default is meaningless for us + pattern = None + + for image, test in tests.items(): + try: + rclass = directory.get_radio(test) + except Exception: + if six.PY3 and 'CHIRP_DEBUG' in os.environ: + LOG.error('Failed to load %s' % test) + continue + raise + for device in _get_sub_devices(rclass, image): + class_name = 'TestCase_%s' % ( + ''.join(filter(lambda c: c.isalnum(), + device.get_name()))) + if isinstance(device, type): + dst = None + else: + dst = device + device = device.__class__ + tc = TestAdapterMeta( + class_name, (TestAdapter,), dict(RADIO_CLASS=device, + SOURCE_IMAGE=image, + RADIO_INST=dst)) + tests = loader.loadTestsFromTestCase(tc) + + if pattern: + tests = [t for t in tests + if re.search(pattern, '%s.%s' % (class_name, + t._testMethodName))] + + suite.addTests(tests) + + return suite diff --git a/tests/icom_clone_simulator.py b/tests/icom_clone_simulator.py new file mode 100644 index 0000000..e13572a --- /dev/null +++ b/tests/icom_clone_simulator.py @@ -0,0 +1,195 @@ +# Copyright 2019 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import struct + +from chirp.drivers import icf + +import logging + +LOG = logging.getLogger(__name__) + + +import sys +l = logging.getLogger() +l.level = logging.ERROR +l.addHandler(logging.StreamHandler(sys.stdout)) + +class FakeIcomRadio(object): + def __init__(self, radio, mapfile=None): + self._buffer = bytes(b'') + self._radio = radio + if not mapfile: + self._memory = bytes(b'\x00') * radio.get_memsize() + else: + self.load_from_file(mapfile) + + def load_from_file(self, filename): + with open(filename, 'rb') as f: + self._memory = bytes(f.read()) + LOG.debug('Initialized %i bytes from %s' % (len(self._memory), + filename)) + + def read(self, count): + """read() from radio, so here we synthesize responses""" + chunk = self._buffer[:count] + self._buffer = self._buffer[count:] + return chunk + + def queue(self, data): + # LOG.debug('Queuing: %r' % data) + self._buffer += data + + def make_response(self, cmd, payload): + return bytes([ + 0xFE, 0xFE, + 0xEF, # Radio + 0xEE, # PC + cmd, + ]) + payload + bytes([0xFD]) + + @property + def address_fmt(self): + if self._radio.get_memsize() > 0x10000: + return 'I' + else: + return 'H' + + def do_clone_out(self): + LOG.debug('Clone from radio started') + size = 16 + for addr in range(0, self._radio.get_memsize(), size): + if len(self._memory[addr:]) < 4: + # IC-W32E has an off-by-one hack for detection, + # which will cause us to send a short one-byte + # block of garbage, unlike the real radio. So, + # if we get to the end and have a few bytes + # left, don't be stupid. + break + header = bytes(struct.pack('>%sB' % self.address_fmt, + addr, size)) + #LOG.debug('Header for %02x@%04x: %r' % ( + # size, addr, header)) + chunk = [] + cs = 0 + for byte in header: + chunk.extend(x for x in bytes(b'%02X' % byte)) + cs += byte + #LOG.debug('Chunk so far: %r' % chunk) + for byte in self._memory[addr:addr + size]: + chunk.extend(x for x in bytes(b'%02X' % byte)) + cs += byte + #LOG.debug('Chunk is %r' % chunk) + + vx = ((cs ^ 0xFFFF) + 1) & 0xFF + chunk.extend(x for x in bytes(b'%02X' % vx)) + self.queue(self.make_response(icf.CMD_CLONE_DAT, bytes(chunk))) + #LOG.debug('Stopping after first frame') + #break + self.queue(self.make_response(icf.CMD_CLONE_END, bytes([]))) + + def do_clone_in(self): + LOG.debug('Clone to radio started') + self._memory = bytes(b'') + + def do_clone_data(self, payload_hex): + if self.address_fmt == 'I': + header_len = 5 + else: + header_len = 3 + + def hex_to_byte(hexchars): + return int('%s%s' % (chr(hexchars[0]), chr(hexchars[1])), 16) + + payload_bytes = bytes([hex_to_byte(payload_hex[i:i+2]) + for i in range(0, len(payload_hex), 2)]) + + addr, size = struct.unpack('>%sB' % self.address_fmt, payload_bytes[:header_len]) + data = payload_bytes[header_len:-1] + csum = payload_bytes[-1] + + #addr_hex = payload[0:size_offset] + #size_hex = payload[size_offset:size_offset + 2] + #data_hex = payload[size_offset + 2:-2] + #csum_hex = payload[-2:] + + + #addr = hex_to_byte(addr_hex[0:2]) << 8 | hex_to_byte(addr_hex[2:4]) + #size = hex_to_byte(size_hex) + #csum = hex_to_byte(csum_hex) + + #data = [] + #for i in range(0, len(data_hex), 2): + # data.append(hex_to_byte(data_hex[i:i+2])) + + if len(data) != size: + LOG.debug('Invalid frame size: expected %i, but got %i' % ( + size, len(data))) + + expected_addr = len(self._memory) + if addr < expected_addr: + LOG.debug('Frame goes back to %04x from %04x' % (addr, + expected_addr)) + if len(self._memory) != addr: + LOG.debug('Filling gap between %04x and %04x' % (expected_addr, + addr)) + self._memory += (bytes(b'\x00') * (addr - expected_addr)) + + # FIXME: Check checksum + + self._memory += data + + def write(self, data): + """write() to radio, so here we process requests""" + + assert isinstance(data, bytes), 'Bytes required, %s received' % data.__class__ + + if data[:12] == (bytes(b'\xFE') * 12): + LOG.debug('Got hispeed kicker') + data = data[12:] + if data[2] == 0xFE: + return + + src = data[2] + dst = data[3] + cmd = data[4] + payload = data[5:-1] + end = data[-1] + + LOG.debug('Received command: %r' % cmd) + LOG.debug(' Full frame: %r' % data) + + model = self._radio.get_model() + bytes(b'\x00' * 20) + + if cmd == 0xE0: # Ident + # FIXME + self.queue(self.make_response(0x01, # Model + model)) + elif cmd == icf.CMD_CLONE_OUT: + self.do_clone_out() + elif cmd == icf.CMD_CLONE_IN: + self.do_clone_in() + elif cmd == icf.CMD_CLONE_DAT: + self.do_clone_data(payload) + else: + LOG.debug('Unknown command %i' % cmd) + self.queue(self.make_response(0x00, bytes([0x01]))) + + return len(data) + + def flush(self): + return diff --git a/tests/images/Alinco_DJ-G7EG.img b/tests/images/Alinco_DJ-G7EG.img new file mode 100644 index 0000000..2286bc1 Binary files /dev/null and b/tests/images/Alinco_DJ-G7EG.img differ diff --git a/tests/images/Alinco_DJ175.img b/tests/images/Alinco_DJ175.img new file mode 100644 index 0000000..0c05432 Binary files /dev/null and b/tests/images/Alinco_DJ175.img differ diff --git a/tests/images/Alinco_DJ596.img b/tests/images/Alinco_DJ596.img new file mode 100644 index 0000000..d37be19 Binary files /dev/null and b/tests/images/Alinco_DJ596.img differ diff --git a/tests/images/Alinco_DR235T.img b/tests/images/Alinco_DR235T.img new file mode 100644 index 0000000..8256271 Binary files /dev/null and b/tests/images/Alinco_DR235T.img differ diff --git a/tests/images/AnyTone_OBLTR-8R.img b/tests/images/AnyTone_OBLTR-8R.img new file mode 100644 index 0000000..2f11032 Binary files /dev/null and b/tests/images/AnyTone_OBLTR-8R.img differ diff --git a/tests/images/AnyTone_TERMN-8R.img b/tests/images/AnyTone_TERMN-8R.img new file mode 100644 index 0000000..0c88931 Binary files /dev/null and b/tests/images/AnyTone_TERMN-8R.img differ diff --git a/tests/images/BTECH_GMRS-50X1.img b/tests/images/BTECH_GMRS-50X1.img new file mode 100644 index 0000000..e84780f Binary files /dev/null and b/tests/images/BTECH_GMRS-50X1.img differ diff --git a/tests/images/BTECH_GMRS-V1.img b/tests/images/BTECH_GMRS-V1.img new file mode 100644 index 0000000..afc0a36 Binary files /dev/null and b/tests/images/BTECH_GMRS-V1.img differ diff --git a/tests/images/BTECH_MURS-V1.img b/tests/images/BTECH_MURS-V1.img new file mode 100644 index 0000000..fac635c Binary files /dev/null and b/tests/images/BTECH_MURS-V1.img differ diff --git a/tests/images/BTECH_UV-2501+220.img b/tests/images/BTECH_UV-2501+220.img new file mode 100755 index 0000000..8bf021c Binary files /dev/null and b/tests/images/BTECH_UV-2501+220.img differ diff --git a/tests/images/BTECH_UV-25X2.img b/tests/images/BTECH_UV-25X2.img new file mode 100755 index 0000000..aec97d8 Binary files /dev/null and b/tests/images/BTECH_UV-25X2.img differ diff --git a/tests/images/BTECH_UV-25X4.img b/tests/images/BTECH_UV-25X4.img new file mode 100755 index 0000000..12ce9f9 Binary files /dev/null and b/tests/images/BTECH_UV-25X4.img differ diff --git a/tests/images/BTECH_UV-5001.img b/tests/images/BTECH_UV-5001.img new file mode 100755 index 0000000..377be8b Binary files /dev/null and b/tests/images/BTECH_UV-5001.img differ diff --git a/tests/images/BTECH_UV-50X2.img b/tests/images/BTECH_UV-50X2.img new file mode 100755 index 0000000..6bac4fb Binary files /dev/null and b/tests/images/BTECH_UV-50X2.img differ diff --git a/tests/images/BTECH_UV-50X3.img b/tests/images/BTECH_UV-50X3.img new file mode 100644 index 0000000..6da4293 Binary files /dev/null and b/tests/images/BTECH_UV-50X3.img differ diff --git a/tests/images/BTECH_UV-5X3.img b/tests/images/BTECH_UV-5X3.img new file mode 100755 index 0000000..aa6ad8a Binary files /dev/null and b/tests/images/BTECH_UV-5X3.img differ diff --git a/tests/images/Baofeng_BF-888.img b/tests/images/Baofeng_BF-888.img new file mode 100644 index 0000000..0304ddf Binary files /dev/null and b/tests/images/Baofeng_BF-888.img differ diff --git a/tests/images/Baofeng_BF-A58S.img b/tests/images/Baofeng_BF-A58S.img new file mode 100644 index 0000000..bbcbffc Binary files /dev/null and b/tests/images/Baofeng_BF-A58S.img differ diff --git a/tests/images/Baofeng_BF-T1.img b/tests/images/Baofeng_BF-T1.img new file mode 100644 index 0000000..849896c Binary files /dev/null and b/tests/images/Baofeng_BF-T1.img differ diff --git a/tests/images/Baofeng_F-11.img b/tests/images/Baofeng_F-11.img new file mode 100644 index 0000000..b13f9b4 Binary files /dev/null and b/tests/images/Baofeng_F-11.img differ diff --git a/tests/images/Baofeng_UV-3R.img b/tests/images/Baofeng_UV-3R.img new file mode 100644 index 0000000..8333f02 Binary files /dev/null and b/tests/images/Baofeng_UV-3R.img differ diff --git a/tests/images/Baofeng_UV-5R.img b/tests/images/Baofeng_UV-5R.img new file mode 100644 index 0000000..dcdc3e8 Binary files /dev/null and b/tests/images/Baofeng_UV-5R.img differ diff --git a/tests/images/Baofeng_UV-6R.img b/tests/images/Baofeng_UV-6R.img new file mode 100755 index 0000000..32f5c15 Binary files /dev/null and b/tests/images/Baofeng_UV-6R.img differ diff --git a/tests/images/Baofeng_UV-B5.img b/tests/images/Baofeng_UV-B5.img new file mode 100644 index 0000000..5d69456 Binary files /dev/null and b/tests/images/Baofeng_UV-B5.img differ diff --git a/tests/images/Baojie_BJ-9900.img b/tests/images/Baojie_BJ-9900.img new file mode 100644 index 0000000..905dda3 Binary files /dev/null and b/tests/images/Baojie_BJ-9900.img differ diff --git a/tests/images/Boblov_X3Plus.img b/tests/images/Boblov_X3Plus.img new file mode 100644 index 0000000..07331e4 Binary files /dev/null and b/tests/images/Boblov_X3Plus.img differ diff --git a/tests/images/Feidaxin_FD-268A.img b/tests/images/Feidaxin_FD-268A.img new file mode 100755 index 0000000..52fc11b Binary files /dev/null and b/tests/images/Feidaxin_FD-268A.img differ diff --git a/tests/images/Feidaxin_FD-268B.img b/tests/images/Feidaxin_FD-268B.img new file mode 100755 index 0000000..8de4949 Binary files /dev/null and b/tests/images/Feidaxin_FD-268B.img differ diff --git a/tests/images/Feidaxin_FD-288B.img b/tests/images/Feidaxin_FD-288B.img new file mode 100755 index 0000000..778ff91 Binary files /dev/null and b/tests/images/Feidaxin_FD-288B.img differ diff --git a/tests/images/Generic_CSV.csv b/tests/images/Generic_CSV.csv new file mode 100644 index 0000000..e50f849 --- /dev/null +++ b/tests/images/Generic_CSV.csv @@ -0,0 +1,104 @@ +Location,Name,Frequency,Duplex,Offset,Tone,rToneFreq,cToneFreq,DtcsCode,DtcsPolarity,Mode,TStep,Skip,Bank,Bank Index,URCALL,RPT1CALL,RPT2CALL +25,H-TAC1,443.100000,+,5.000000,DTCS,88.5,88.5,032,NN,FM,5.00,,,-1,,,, +26,H-TAC2,147.380000,+,0.600000,Tone,100.0,100.0,023,NN,FM,5.00,,,-1,,,, +27,H-TAC3,147.440000,,0.600000,Tone,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +28,H-TAC4,441.550000,+,5.000000,Tone,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +29,H-TAC5,442.925000,+,5.000000,Tone,107.2,107.2,023,NN,FM,5.00,,,-1,,,, +30,H-TAC6,443.350000,+,5.000000,Tone,156.7,156.7,023,NN,FM,5.00,,,-1,,,, +31,H-TAC7,442.825000,+,5.000000,,110.9,110.9,023,NN,FM,5.00,,,-1,,,, +50,ARESD1,147.320000,+,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +51,WAPRIR,146.900000,-,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +52,WAPRIS,147.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +53,WASECR,440.350000,+,5.000000,TSQL,127.3,127.3,023,NN,FM,5.00,,,-1,,,, +54,OEMNCS,145.330000,-,0.600000,Tone,186.2,186.2,023,NN,FM,5.00,,,-1,,,, +55,HEARTN,145.230000,-,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +56,CLACK,147.120000,+,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +57,CLATSP,146.660000,-,0.600000,Tone,118.8,118.8,023,NN,FM,5.00,,,-1,,,, +58,COLUMB,146.880000,-,0.600000,Tone,114.8,114.8,023,NN,FM,5.00,,,-1,,,, +59,TMOOK1,147.220000,+,0.600000,Tone,100.0,100.0,023,NN,FM,5.00,,,-1,,,, +60,TMOOK2,147.160000,+,0.600000,Tone,118.8,118.8,023,NN,FM,5.00,,,-1,,,, +61,TMOOK3,440.175000,+,5.000000,Tone,100.0,100.0,023,NN,FM,5.00,,,-1,,,, +62,TMOOK4,441.250000,+,5.000000,Tone,118.8,118.8,023,NN,FM,5.00,,,-1,,,, +63,MULTNM,146.840000,-,0.600000,Tone,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +64,CLARK,147.240000,+,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +65,ARC,146.980000,-,0.600000,Tone,123.0,123.0,023,NN,FM,5.00,,,-1,,,, +66,VERNIA,145.250000,-,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +80,WX1,162.400000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +81,WX2,162.425000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +82,WX3,162.450000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +83,WX4,162.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +84,WX5,162.500000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +85,WX6,162.525000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +86,WX7,162.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +88,CTAF,119.300000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +89,ATIS,127.650000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +90,GROUND,121.700000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +91,TOWER,119.300000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +92,UNICOM,122.950000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +93,ACARS,131.550000,,0.600000,,88.5,88.5,023,NN,AM,5.00,,,-1,,,, +100,ICALL,851.012500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +101,ITAC1,851.512500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +102,ITAC2,852.012500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +103,ITAC3,852.512500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +104,ITAC4,853.012500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +105,OROPS1,851.325000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +106,OROPS2,851.387500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +107,OROPS3,851.750000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +108,OROPS4,851.775000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +109,OROPS5,851.800000,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +110,WAOPS1,852.537500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +111,WAOPS2,852.562500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +112,WAOPS3,852.587500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +113,WAOPS4,852.612500,,0.600000,TSQL,88.5,156.7,023,NN,FM,10.00,,,-1,,,, +114,WAOPS5,852.637500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +115,UCAL40,453.212500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +116,UTAC41,453.462500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +117,UTAC42,453.712500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +118,UTAC43,453.862500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +119,ISIMP1,853.437500,,0.600000,DTCS,88.5,88.5,074,NN,FM,12.50,,,-1,,,, +120,ISIMP2,851.037500,,0.600000,DTCS,88.5,88.5,114,NN,FM,12.50,,,-1,,,, +121,ISIMP3,851.950000,,0.600000,DTCS,88.5,88.5,131,NN,FM,10.00,,,-1,,,, +122,ISIMP4,851.175000,,0.600000,DTCS,88.5,88.5,023,NN,FM,10.00,,,-1,,,, +123,MAYDAY,853.387500,,0.600000,DTCS,88.5,88.5,025,NN,FM,12.50,,,-1,,,, +124,VCALL,155.750000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +125,VTAC11,151.137500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +126,VTAC12,154.452500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +127,VTAC13,158.737500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +128,VTAC14,159.472500,,0.600000,TSQL,88.5,156.7,023,NN,FM,12.50,,,-1,,,, +129,WCCCA1,860.737500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +130,WCCCA2,860.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +131,WCCCA3,859.737500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +132,WCCCA4,859.737500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +133,WCCCA5,858.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +134,WCCCA6,857.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +135,WCCCA7,856.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +136,WCCCA8,855.962500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +137,WCCCA9,855.237500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +138,WCCCA0,854.987500,,0.600000,,88.5,88.5,023,NN,FM,12.50,,,-1,,,, +139,OR SAR,155.805000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +140,OPEN,155.475000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +141,OSPTAC,156.030000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +142,OSPD1A,154.935000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +143,OSPD1B,156.225000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +144,OSPD1C,154.905000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +145,OSPD1D,156.150000,,0.600000,TSQL,88.5,179.9,023,NN,FM,5.00,,,-1,,,, +146,OSPD6A,153.935000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +147,OSPD6B,154.785000,,0.600000,TSQL,88.5,156.7,023,NN,FM,5.00,,,-1,,,, +148,OSPD6C,154.860000,,0.600000,TSQL,88.5,131.8,023,NN,FM,5.00,,,-1,,,, +149,OSPD6D,155.910000,,0.600000,TSQL,88.5,131.8,023,NN,FM,5.00,,,-1,,,, +150,WASP,155.370000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +151,CHP,156.075000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +152,ODFW,158.895000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +153,SARINTOP,158.905000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +160,H-DS1,147.550000,,0.600000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +161,H-DS2,147.580000,,0.600000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +162,H-DS3,446.300000,,5.000000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +163,H-DS4,446.400000,,5.000000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +164,H-DS5,1294.100000,,0.600000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +165,H-DS6,441.637500,+,5.000000,,88.5,88.5,023,NN,DV,12.50,,,-1,CQCQCQ,,,0 +166,H-DS7,440.550000,+,5.000000,,88.5,88.5,023,NN,DV,5.00,,,-1,CQCQCQ,,,0 +167,H-DS8,444.262500,+,5.000000,,88.5,88.5,023,NN,DV,12.50,,,-1,CQCQCQ,,,0 +168,HPAGE,145.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +169,APRS,144.390000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +170,H-DAT1,145.550000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, +171,H-DAT2,145.070000,,0.600000,,88.5,88.5,023,NN,FM,5.00,,,-1,,,, diff --git a/tests/images/Icom_IC-208H.img b/tests/images/Icom_IC-208H.img new file mode 100644 index 0000000..8917475 Binary files /dev/null and b/tests/images/Icom_IC-208H.img differ diff --git a/tests/images/Icom_IC-2100H.img b/tests/images/Icom_IC-2100H.img new file mode 100644 index 0000000..cf6ffd8 Binary files /dev/null and b/tests/images/Icom_IC-2100H.img differ diff --git a/tests/images/Icom_IC-2200H.img b/tests/images/Icom_IC-2200H.img new file mode 100644 index 0000000..56ee266 Binary files /dev/null and b/tests/images/Icom_IC-2200H.img differ diff --git a/tests/images/Icom_IC-2300H.img b/tests/images/Icom_IC-2300H.img new file mode 100644 index 0000000..cd6aad0 Binary files /dev/null and b/tests/images/Icom_IC-2300H.img differ diff --git a/tests/images/Icom_IC-2720H.img b/tests/images/Icom_IC-2720H.img new file mode 100644 index 0000000..e153ce1 Binary files /dev/null and b/tests/images/Icom_IC-2720H.img differ diff --git a/tests/images/Icom_IC-2730A.img b/tests/images/Icom_IC-2730A.img new file mode 100644 index 0000000..73b973a Binary files /dev/null and b/tests/images/Icom_IC-2730A.img differ diff --git a/tests/images/Icom_IC-2820H.img b/tests/images/Icom_IC-2820H.img new file mode 100644 index 0000000..e845f9f Binary files /dev/null and b/tests/images/Icom_IC-2820H.img differ diff --git a/tests/images/Icom_IC-P7.img b/tests/images/Icom_IC-P7.img new file mode 100755 index 0000000..35a0966 Binary files /dev/null and b/tests/images/Icom_IC-P7.img differ diff --git a/tests/images/Icom_IC-Q7A.img b/tests/images/Icom_IC-Q7A.img new file mode 100644 index 0000000..bad5f87 Binary files /dev/null and b/tests/images/Icom_IC-Q7A.img differ diff --git a/tests/images/Icom_IC-T70.img b/tests/images/Icom_IC-T70.img new file mode 100644 index 0000000..5c09019 Binary files /dev/null and b/tests/images/Icom_IC-T70.img differ diff --git a/tests/images/Icom_IC-T7H.img b/tests/images/Icom_IC-T7H.img new file mode 100644 index 0000000..5186620 Binary files /dev/null and b/tests/images/Icom_IC-T7H.img differ diff --git a/tests/images/Icom_IC-T8A.img b/tests/images/Icom_IC-T8A.img new file mode 100644 index 0000000..c5bcc45 Binary files /dev/null and b/tests/images/Icom_IC-T8A.img differ diff --git a/tests/images/Icom_IC-V82_U82.img b/tests/images/Icom_IC-V82_U82.img new file mode 100644 index 0000000..5a1c0a4 Binary files /dev/null and b/tests/images/Icom_IC-V82_U82.img differ diff --git a/tests/images/Icom_IC-W32A.img b/tests/images/Icom_IC-W32A.img new file mode 100644 index 0000000..b5b239c Binary files /dev/null and b/tests/images/Icom_IC-W32A.img differ diff --git a/tests/images/Icom_IC-W32E.img b/tests/images/Icom_IC-W32E.img new file mode 100644 index 0000000..32bee84 Binary files /dev/null and b/tests/images/Icom_IC-W32E.img differ diff --git a/tests/images/Icom_ID-31A.img b/tests/images/Icom_ID-31A.img new file mode 100644 index 0000000..afdebdd Binary files /dev/null and b/tests/images/Icom_ID-31A.img differ diff --git a/tests/images/Icom_ID-51.img b/tests/images/Icom_ID-51.img new file mode 100644 index 0000000..399c857 Binary files /dev/null and b/tests/images/Icom_ID-51.img differ diff --git a/tests/images/Icom_ID-51_Plus.img b/tests/images/Icom_ID-51_Plus.img new file mode 100755 index 0000000..07bdc7e Binary files /dev/null and b/tests/images/Icom_ID-51_Plus.img differ diff --git a/tests/images/Icom_ID-800H_v2.img b/tests/images/Icom_ID-800H_v2.img new file mode 100644 index 0000000..113f450 Binary files /dev/null and b/tests/images/Icom_ID-800H_v2.img differ diff --git a/tests/images/Icom_ID-880H.img b/tests/images/Icom_ID-880H.img new file mode 100644 index 0000000..0a83f24 Binary files /dev/null and b/tests/images/Icom_ID-880H.img differ diff --git a/tests/images/Jetstream_JT220M.img b/tests/images/Jetstream_JT220M.img new file mode 100644 index 0000000..bb0b4a7 Binary files /dev/null and b/tests/images/Jetstream_JT220M.img differ diff --git a/tests/images/Jetstream_JT270M.img b/tests/images/Jetstream_JT270M.img new file mode 100644 index 0000000..acc95d4 Binary files /dev/null and b/tests/images/Jetstream_JT270M.img differ diff --git a/tests/images/Jetstream_JT270MH.img b/tests/images/Jetstream_JT270MH.img new file mode 100644 index 0000000..4027cb0 Binary files /dev/null and b/tests/images/Jetstream_JT270MH.img differ diff --git a/tests/images/KYD_IP-620.img b/tests/images/KYD_IP-620.img new file mode 100755 index 0000000..c088dfe Binary files /dev/null and b/tests/images/KYD_IP-620.img differ diff --git a/tests/images/KYD_NC-630A.img b/tests/images/KYD_NC-630A.img new file mode 100755 index 0000000..2c627fa Binary files /dev/null and b/tests/images/KYD_NC-630A.img differ diff --git a/tests/images/Kenwood_HMK.hmk b/tests/images/Kenwood_HMK.hmk new file mode 100644 index 0000000..e8ee04a --- /dev/null +++ b/tests/images/Kenwood_HMK.hmk @@ -0,0 +1,71 @@ +KENWOOD MCP FOR AMATEUR MOBILE TRANSCEIVER +[Export Software]=MCP-2A Version 3.02 +[Export File Version]=1 +[Type]=E +[Language]=English + +// Comments +!!Comments= + +// Memory Channels +!!Ch,Rx Freq.,Rx Step,Offset,T/CT/DCS,TO Freq.,CT Freq.,DCS Code,Shift/Split,Rev.,L.Out,Mode,Tx Freq.,Tx Step,M.Name +"900","00155,500000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,500000","025,00","MVHF L1" +"901","00155,525000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,525000","025,00","MVHF L2" +"902","00155,625000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,625000","025,00","MVHF F1" +"903","00155,775000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,775000","025,00","MVHF F2" +"904","00155,825000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","155,825000","025,00","MVHF F3" +"905","00156,300000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,300000","025,00","MVHF K06" +"906","00156,375000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,375000","025,00","MVHF K67" +"907","00156,400000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,400000","025,00","MVHF K08" +"908","00156,425000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,425000","025,00","MVHF K68" +"909","00156,450000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,450000","025,00","MVHF K09" +"910","00156,475000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,475000","025,00","MVHF K69" +"911","00156,500000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,500000","025,00","MVHF K10" +"912","00156,525000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,525000","025,00","MDSC K70" +"913","00156,550000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,550000","025,00","MVHF K11" +"914","00156,575000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,575000","025,00","MVHF K71" +"915","00156,600000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,600000","025,00","MVHF K12" +"916","00156,625000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,625000","025,00","MVHF K72" +"917","00156,650000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,650000","025,00","MVHF K13" +"918","00156,675000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,675000","025,00","MVHF K73" +"919","00156,700000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,700000","025,00","MVHF K14" +"920","00156,725000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,725000","025,00","MVHF K74" +"921","00156,750000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,750000","025,00","MVHF K15" +"922","00156,800000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,800000","025,00","MVHF K16" +"923","00156,850000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,850000","025,00","MVHF K17" +"924","00156,875000","025,00","00,000000","Off","88,5","88,5","023"," ","Off","Off","FM","156,875000","025,00","MVHF K77" +"925","00160,625000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,625000","025,00","MVHF K60" +"926","00160,650000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,650000","025,00","MVHF K01" +"927","00160,675000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,675000","025,00","MVHF K61" +"928","00160,700000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,700000","025,00","MVHF K02" +"929","00160,725000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,725000","025,00","MVHF K62" +"930","00160,750000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,750000","025,00","MVHF K03" +"931","00160,775000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,775000","025,00","MVHF K63" +"932","00160,800000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,800000","025,00","MVHF K04" +"933","00160,825000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,825000","025,00","MVHF K64" +"934","00160,850000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,850000","025,00","MVHF K05" +"935","00160,875000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,875000","025,00","MVHF K65" +"936","00160,925000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,925000","025,00","MVHF K66" +"937","00160,950000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","160,950000","025,00","MVHF K07" +"938","00161,500000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,500000","025,00","MVHF K18" +"939","00161,525000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,525000","025,00","MVHF K78" +"940","00161,550000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,550000","025,00","MVHF K19" +"941","00161,575000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,575000","025,00","MVHF K79" +"942","00161,600000","025,00","04,600000","Off","88,5","88,5","023","-","Off","Off","FM","161,600000","025,00","MVHF K20" +"943","00161,625000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,625000","025,00","MVHF K80" +"944","00161,650000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,650000","025,00","MVHF K21" +"945","00161,675000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,675000","025,00","MVHF K81" +"946","00161,700000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,700000","025,00","MVHF K22" +"947","00161,725000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,725000","025,00","MVHF K82" +"948","00161,750000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,750000","025,00","MVHF K23" +"949","00161,775000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,775000","025,00","MVHF K83" +"950","00161,800000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,800000","025,00","MVHF K24" +"951","00161,825000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,825000","025,00","MVHF K84" +"952","00161,850000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,850000","025,00","MVHF K25" +"953","00161,875000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,875000","025,00","MVHF K85" +"954","00161,900000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,900000","025,00","MVHF K26" +"955","00161,925000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,925000","025,00","MVHF K86" +"956","00161,950000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,950000","025,00","MVHF K27" +"957","00161,975000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","161,975000","025,00","MAIS K87" +"958","00162,000000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","162,000000","025,00","MVHF K28" +"959","00162,025000","025,00","04,600000","Off","88,5","88,5","023","-","Off","On","FM","162,025000","025,00","MAIS K88" diff --git a/tests/images/Kenwood_TH-D72_clone_mode.img b/tests/images/Kenwood_TH-D72_clone_mode.img new file mode 100644 index 0000000..257c2a2 Binary files /dev/null and b/tests/images/Kenwood_TH-D72_clone_mode.img differ diff --git a/tests/images/Kenwood_TK-272G.img b/tests/images/Kenwood_TK-272G.img new file mode 100755 index 0000000..bdeed1f Binary files /dev/null and b/tests/images/Kenwood_TK-272G.img differ diff --git a/tests/images/Kenwood_TK-3180K2.img b/tests/images/Kenwood_TK-3180K2.img new file mode 100644 index 0000000..b3524c9 Binary files /dev/null and b/tests/images/Kenwood_TK-3180K2.img differ diff --git a/tests/images/Kenwood_TK-760G.img b/tests/images/Kenwood_TK-760G.img new file mode 100755 index 0000000..23c883a Binary files /dev/null and b/tests/images/Kenwood_TK-760G.img differ diff --git a/tests/images/Kenwood_TK-8102.img b/tests/images/Kenwood_TK-8102.img new file mode 100644 index 0000000..d03fbc4 Binary files /dev/null and b/tests/images/Kenwood_TK-8102.img differ diff --git a/tests/images/Kenwood_TK-8180.img b/tests/images/Kenwood_TK-8180.img new file mode 100644 index 0000000..06a5910 Binary files /dev/null and b/tests/images/Kenwood_TK-8180.img differ diff --git a/tests/images/Kenwood_TS-480_CloneMode.img b/tests/images/Kenwood_TS-480_CloneMode.img new file mode 100644 index 0000000..2e018a0 Binary files /dev/null and b/tests/images/Kenwood_TS-480_CloneMode.img differ diff --git a/tests/images/LUITON_LT-725UV.img b/tests/images/LUITON_LT-725UV.img new file mode 100755 index 0000000..537e13b Binary files /dev/null and b/tests/images/LUITON_LT-725UV.img differ diff --git a/tests/images/Leixen_VV-898.img b/tests/images/Leixen_VV-898.img new file mode 100644 index 0000000..abeec14 Binary files /dev/null and b/tests/images/Leixen_VV-898.img differ diff --git a/tests/images/Leixen_VV-898S.img b/tests/images/Leixen_VV-898S.img new file mode 100755 index 0000000..10a264a Binary files /dev/null and b/tests/images/Leixen_VV-898S.img differ diff --git a/tests/images/Polmar_DB-50M.img b/tests/images/Polmar_DB-50M.img new file mode 100644 index 0000000..04df657 Binary files /dev/null and b/tests/images/Polmar_DB-50M.img differ diff --git a/tests/images/Puxing_PX-2R.img b/tests/images/Puxing_PX-2R.img new file mode 100644 index 0000000..63c615c Binary files /dev/null and b/tests/images/Puxing_PX-2R.img differ diff --git a/tests/images/Puxing_PX-777.img b/tests/images/Puxing_PX-777.img new file mode 100644 index 0000000..3fceb43 Binary files /dev/null and b/tests/images/Puxing_PX-777.img differ diff --git a/tests/images/Puxing_PX-888K.img b/tests/images/Puxing_PX-888K.img new file mode 100644 index 0000000..27d30d0 Binary files /dev/null and b/tests/images/Puxing_PX-888K.img differ diff --git a/tests/images/QYT_KT7900D.img b/tests/images/QYT_KT7900D.img new file mode 100755 index 0000000..73bfc37 Binary files /dev/null and b/tests/images/QYT_KT7900D.img differ diff --git a/tests/images/QYT_KT8900D.img b/tests/images/QYT_KT8900D.img new file mode 100755 index 0000000..e31310f Binary files /dev/null and b/tests/images/QYT_KT8900D.img differ diff --git a/tests/images/Radioddity_R2.img b/tests/images/Radioddity_R2.img new file mode 100644 index 0000000..0bbd846 Binary files /dev/null and b/tests/images/Radioddity_R2.img differ diff --git a/tests/images/Radtel_T18.img b/tests/images/Radtel_T18.img new file mode 100755 index 0000000..f508675 Binary files /dev/null and b/tests/images/Radtel_T18.img differ diff --git a/tests/images/Retevis_RT21.img b/tests/images/Retevis_RT21.img new file mode 100644 index 0000000..030d4f7 Binary files /dev/null and b/tests/images/Retevis_RT21.img differ diff --git a/tests/images/Retevis_RT22.img b/tests/images/Retevis_RT22.img new file mode 100644 index 0000000..ffca933 Binary files /dev/null and b/tests/images/Retevis_RT22.img differ diff --git a/tests/images/Retevis_RT23.img b/tests/images/Retevis_RT23.img new file mode 100755 index 0000000..2bb0252 Binary files /dev/null and b/tests/images/Retevis_RT23.img differ diff --git a/tests/images/Retevis_RT26.img b/tests/images/Retevis_RT26.img new file mode 100644 index 0000000..fdb6141 Binary files /dev/null and b/tests/images/Retevis_RT26.img differ diff --git a/tests/images/TDXone_TD-Q8A.img b/tests/images/TDXone_TD-Q8A.img new file mode 100755 index 0000000..6bd3d07 Binary files /dev/null and b/tests/images/TDXone_TD-Q8A.img differ diff --git a/tests/images/TYT_TH-350.img b/tests/images/TYT_TH-350.img new file mode 100644 index 0000000..7d1fd3c Binary files /dev/null and b/tests/images/TYT_TH-350.img differ diff --git a/tests/images/TYT_TH-7800.img b/tests/images/TYT_TH-7800.img new file mode 100644 index 0000000..5c9240d Binary files /dev/null and b/tests/images/TYT_TH-7800.img differ diff --git a/tests/images/TYT_TH-9800.img b/tests/images/TYT_TH-9800.img new file mode 100644 index 0000000..a2f584b Binary files /dev/null and b/tests/images/TYT_TH-9800.img differ diff --git a/tests/images/TYT_TH-UV3R-25.img b/tests/images/TYT_TH-UV3R-25.img new file mode 100755 index 0000000..d62a459 Binary files /dev/null and b/tests/images/TYT_TH-UV3R-25.img differ diff --git a/tests/images/TYT_TH-UV3R.img b/tests/images/TYT_TH-UV3R.img new file mode 100644 index 0000000..75b2cf6 Binary files /dev/null and b/tests/images/TYT_TH-UV3R.img differ diff --git a/tests/images/TYT_TH-UV8000.img b/tests/images/TYT_TH-UV8000.img new file mode 100644 index 0000000..71b7aa9 Binary files /dev/null and b/tests/images/TYT_TH-UV8000.img differ diff --git a/tests/images/TYT_TH-UVF1.img b/tests/images/TYT_TH-UVF1.img new file mode 100644 index 0000000..f01edd6 Binary files /dev/null and b/tests/images/TYT_TH-UVF1.img differ diff --git a/tests/images/TYT_TH9000_144.img b/tests/images/TYT_TH9000_144.img new file mode 100644 index 0000000..871e986 Binary files /dev/null and b/tests/images/TYT_TH9000_144.img differ diff --git a/tests/images/Vertex_Standard_VXA-700.img b/tests/images/Vertex_Standard_VXA-700.img new file mode 100644 index 0000000..e0dbbc0 Binary files /dev/null and b/tests/images/Vertex_Standard_VXA-700.img differ diff --git a/tests/images/WACCOM_MINI-8900.img b/tests/images/WACCOM_MINI-8900.img new file mode 100755 index 0000000..9b5737a Binary files /dev/null and b/tests/images/WACCOM_MINI-8900.img differ diff --git a/tests/images/Wouxun_KG-816.img b/tests/images/Wouxun_KG-816.img new file mode 100644 index 0000000..090e18d Binary files /dev/null and b/tests/images/Wouxun_KG-816.img differ diff --git a/tests/images/Wouxun_KG-818.img b/tests/images/Wouxun_KG-818.img new file mode 100644 index 0000000..716da73 Binary files /dev/null and b/tests/images/Wouxun_KG-818.img differ diff --git a/tests/images/Wouxun_KG-UV6.img b/tests/images/Wouxun_KG-UV6.img new file mode 100644 index 0000000..b1a7795 Binary files /dev/null and b/tests/images/Wouxun_KG-UV6.img differ diff --git a/tests/images/Wouxun_KG-UV8D.img b/tests/images/Wouxun_KG-UV8D.img new file mode 100644 index 0000000..1c66447 Binary files /dev/null and b/tests/images/Wouxun_KG-UV8D.img differ diff --git a/tests/images/Wouxun_KG-UV8D_Plus.img b/tests/images/Wouxun_KG-UV8D_Plus.img new file mode 100644 index 0000000..23e6f58 Binary files /dev/null and b/tests/images/Wouxun_KG-UV8D_Plus.img differ diff --git a/tests/images/Wouxun_KG-UV8E.img b/tests/images/Wouxun_KG-UV8E.img new file mode 100644 index 0000000..89e5e1f Binary files /dev/null and b/tests/images/Wouxun_KG-UV8E.img differ diff --git a/tests/images/Wouxun_KG-UV9D_Plus.img b/tests/images/Wouxun_KG-UV9D_Plus.img new file mode 100644 index 0000000..410e498 Binary files /dev/null and b/tests/images/Wouxun_KG-UV9D_Plus.img differ diff --git a/tests/images/Wouxun_KG-UVD1P.img b/tests/images/Wouxun_KG-UVD1P.img new file mode 100644 index 0000000..93df646 Binary files /dev/null and b/tests/images/Wouxun_KG-UVD1P.img differ diff --git a/tests/images/Yaesu_FT-1500M.img b/tests/images/Yaesu_FT-1500M.img new file mode 100644 index 0000000..581e353 Binary files /dev/null and b/tests/images/Yaesu_FT-1500M.img differ diff --git a/tests/images/Yaesu_FT-1802M.img b/tests/images/Yaesu_FT-1802M.img new file mode 100644 index 0000000..f13e34d Binary files /dev/null and b/tests/images/Yaesu_FT-1802M.img differ diff --git a/tests/images/Yaesu_FT-1D_R.img b/tests/images/Yaesu_FT-1D_R.img new file mode 100644 index 0000000..70190c8 Binary files /dev/null and b/tests/images/Yaesu_FT-1D_R.img differ diff --git a/tests/images/Yaesu_FT-25R.img b/tests/images/Yaesu_FT-25R.img new file mode 100644 index 0000000..fef4b0e Binary files /dev/null and b/tests/images/Yaesu_FT-25R.img differ diff --git a/tests/images/Yaesu_FT-2800M.img b/tests/images/Yaesu_FT-2800M.img new file mode 100644 index 0000000..35ccf18 Binary files /dev/null and b/tests/images/Yaesu_FT-2800M.img differ diff --git a/tests/images/Yaesu_FT-2900R_1900R.img b/tests/images/Yaesu_FT-2900R_1900R.img new file mode 100755 index 0000000..ef17a61 Binary files /dev/null and b/tests/images/Yaesu_FT-2900R_1900R.img differ diff --git a/tests/images/Yaesu_FT-450D.img b/tests/images/Yaesu_FT-450D.img new file mode 100644 index 0000000..09873ea Binary files /dev/null and b/tests/images/Yaesu_FT-450D.img differ diff --git a/tests/images/Yaesu_FT-4VR.img b/tests/images/Yaesu_FT-4VR.img new file mode 100644 index 0000000..ffa6cfe Binary files /dev/null and b/tests/images/Yaesu_FT-4VR.img differ diff --git a/tests/images/Yaesu_FT-4XE.img b/tests/images/Yaesu_FT-4XE.img new file mode 100644 index 0000000..fb8087a Binary files /dev/null and b/tests/images/Yaesu_FT-4XE.img differ diff --git a/tests/images/Yaesu_FT-4XR.img b/tests/images/Yaesu_FT-4XR.img new file mode 100644 index 0000000..b45d1ae Binary files /dev/null and b/tests/images/Yaesu_FT-4XR.img differ diff --git a/tests/images/Yaesu_FT-50.img b/tests/images/Yaesu_FT-50.img new file mode 100755 index 0000000..77cffd7 Binary files /dev/null and b/tests/images/Yaesu_FT-50.img differ diff --git a/tests/images/Yaesu_FT-60.img b/tests/images/Yaesu_FT-60.img new file mode 100644 index 0000000..f17d470 Binary files /dev/null and b/tests/images/Yaesu_FT-60.img differ diff --git a/tests/images/Yaesu_FT-65E.img b/tests/images/Yaesu_FT-65E.img new file mode 100644 index 0000000..94cda1a Binary files /dev/null and b/tests/images/Yaesu_FT-65E.img differ diff --git a/tests/images/Yaesu_FT-65R.img b/tests/images/Yaesu_FT-65R.img new file mode 100644 index 0000000..c62c3df Binary files /dev/null and b/tests/images/Yaesu_FT-65R.img differ diff --git a/tests/images/Yaesu_FT-70D.img b/tests/images/Yaesu_FT-70D.img new file mode 100644 index 0000000..b557586 Binary files /dev/null and b/tests/images/Yaesu_FT-70D.img differ diff --git a/tests/images/Yaesu_FT-7100M.img b/tests/images/Yaesu_FT-7100M.img new file mode 100644 index 0000000..30e18ce Binary files /dev/null and b/tests/images/Yaesu_FT-7100M.img differ diff --git a/tests/images/Yaesu_FT-7800_7900.img b/tests/images/Yaesu_FT-7800_7900.img new file mode 100644 index 0000000..c10a4fb Binary files /dev/null and b/tests/images/Yaesu_FT-7800_7900.img differ diff --git a/tests/images/Yaesu_FT-817.img b/tests/images/Yaesu_FT-817.img new file mode 100644 index 0000000..00e624d Binary files /dev/null and b/tests/images/Yaesu_FT-817.img differ diff --git a/tests/images/Yaesu_FT-817ND.img b/tests/images/Yaesu_FT-817ND.img new file mode 100644 index 0000000..c0a2bb4 Binary files /dev/null and b/tests/images/Yaesu_FT-817ND.img differ diff --git a/tests/images/Yaesu_FT-817ND_US.img b/tests/images/Yaesu_FT-817ND_US.img new file mode 100644 index 0000000..210c5a8 Binary files /dev/null and b/tests/images/Yaesu_FT-817ND_US.img differ diff --git a/tests/images/Yaesu_FT-818.img b/tests/images/Yaesu_FT-818.img new file mode 100644 index 0000000..59b6e62 Binary files /dev/null and b/tests/images/Yaesu_FT-818.img differ diff --git a/tests/images/Yaesu_FT-857_897.img b/tests/images/Yaesu_FT-857_897.img new file mode 100644 index 0000000..ac66f73 Binary files /dev/null and b/tests/images/Yaesu_FT-857_897.img differ diff --git a/tests/images/Yaesu_FT-857_897_US.img b/tests/images/Yaesu_FT-857_897_US.img new file mode 100644 index 0000000..20a1c24 Binary files /dev/null and b/tests/images/Yaesu_FT-857_897_US.img differ diff --git a/tests/images/Yaesu_FT-8800.img b/tests/images/Yaesu_FT-8800.img new file mode 100644 index 0000000..4cbb883 Binary files /dev/null and b/tests/images/Yaesu_FT-8800.img differ diff --git a/tests/images/Yaesu_FT-8900.img b/tests/images/Yaesu_FT-8900.img new file mode 100644 index 0000000..0445467 Binary files /dev/null and b/tests/images/Yaesu_FT-8900.img differ diff --git a/tests/images/Yaesu_FT2D_R.img b/tests/images/Yaesu_FT2D_R.img new file mode 100644 index 0000000..459b0ce Binary files /dev/null and b/tests/images/Yaesu_FT2D_R.img differ diff --git a/tests/images/Yaesu_FT3D_R.img b/tests/images/Yaesu_FT3D_R.img new file mode 100644 index 0000000..179d69e Binary files /dev/null and b/tests/images/Yaesu_FT3D_R.img differ diff --git a/tests/images/Yaesu_FTM-3200D_R.img b/tests/images/Yaesu_FTM-3200D_R.img new file mode 100644 index 0000000..d3f3f66 Binary files /dev/null and b/tests/images/Yaesu_FTM-3200D_R.img differ diff --git a/tests/images/Yaesu_FTM-350.img b/tests/images/Yaesu_FTM-350.img new file mode 100644 index 0000000..1e19c47 Binary files /dev/null and b/tests/images/Yaesu_FTM-350.img differ diff --git a/tests/images/Yaesu_VX-2.img b/tests/images/Yaesu_VX-2.img new file mode 100644 index 0000000..bc237fd Binary files /dev/null and b/tests/images/Yaesu_VX-2.img differ diff --git a/tests/images/Yaesu_VX-3.img b/tests/images/Yaesu_VX-3.img new file mode 100644 index 0000000..eedefd3 Binary files /dev/null and b/tests/images/Yaesu_VX-3.img differ diff --git a/tests/images/Yaesu_VX-5.img b/tests/images/Yaesu_VX-5.img new file mode 100644 index 0000000..522c0f3 Binary files /dev/null and b/tests/images/Yaesu_VX-5.img differ diff --git a/tests/images/Yaesu_VX-6.img b/tests/images/Yaesu_VX-6.img new file mode 100644 index 0000000..509099d Binary files /dev/null and b/tests/images/Yaesu_VX-6.img differ diff --git a/tests/images/Yaesu_VX-7.img b/tests/images/Yaesu_VX-7.img new file mode 100644 index 0000000..2542f8e Binary files /dev/null and b/tests/images/Yaesu_VX-7.img differ diff --git a/tests/images/Yaesu_VX-8DR.img b/tests/images/Yaesu_VX-8DR.img new file mode 100644 index 0000000..299fdc1 Binary files /dev/null and b/tests/images/Yaesu_VX-8DR.img differ diff --git a/tests/images/Yaesu_VX-8GE.img b/tests/images/Yaesu_VX-8GE.img new file mode 100644 index 0000000..a140121 Binary files /dev/null and b/tests/images/Yaesu_VX-8GE.img differ diff --git a/tests/images/Yaesu_VX-8R.img b/tests/images/Yaesu_VX-8R.img new file mode 100644 index 0000000..0ae38f7 Binary files /dev/null and b/tests/images/Yaesu_VX-8R.img differ diff --git a/tests/run_tests b/tests/run_tests new file mode 100755 index 0000000..226de0c --- /dev/null +++ b/tests/run_tests @@ -0,0 +1,3 @@ +#!/bin/bash + +exec python $(readlink -f $0).py $* diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100755 index 0000000..e13fd8c --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,1372 @@ +#!/usr/bin/env python +# +# Copyright 2011 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from __future__ import print_function +from builtins import bytes +import copy +import traceback +import sys +import os +import shutil +import glob +import tempfile +import time +from optparse import OptionParser +from serial import Serial + +# change to the tests directory +scriptdir = os.path.dirname(sys.argv[0]) +if scriptdir: + os.chdir(scriptdir) + +sys.path.insert(0, "../") + +os.environ['CHIRP_TESTENV'] = 'sigh' +import logging +from chirp import logger + +class LoggerOpts(object): + quiet = 2 + verbose = 0 + log_file = os.path.join('logs', 'debug.log') + log_level = logging.DEBUG + +if not os.path.exists("logs"): + os.mkdir("logs") +logger.handle_options(LoggerOpts()) + +from chirp import CHIRP_VERSION +# FIXME: Not all drivers are py3 compatible in syntax, so punt on this +# until that time, and defer to the safe import loop below. +# from chirp.drivers import * +from chirp import chirp_common, directory +from chirp import import_logic, memmap, settings, errors +from chirp import settings + +directory.safe_import_drivers() + +from chirp.drivers import generic_csv + +TESTS = {} + +time.sleep = lambda s: None + + +class TestError(Exception): + def get_detail(self): + return str(self) + + +class TestInternalError(TestError): + pass + + +class TestCrashError(TestError): + def __init__(self, tb, exc, args): + Exception.__init__(self, str(exc)) + self.__tb = tb + self.__exc = exc + self.__args = args + self.__mytb = "".join(traceback.format_stack()) + + def __str__(self): + return str(self.__exc) + + def get_detail(self): + return str(self.__exc) + os.linesep + \ + ("Args were: %s" % self.__args) + os.linesep + \ + self.__tb + os.linesep + \ + "Called from:" + os.linesep + self.__mytb + + def get_original_exception(self): + return self.__exc + + +class TestFailedError(TestError): + def __init__(self, msg, detail=""): + TestError.__init__(self, msg) + self._detail = detail + + def get_detail(self): + return self._detail + + +class TestSkippedError(TestError): + pass + + +def get_tb(): + return traceback.format_exc() + + +class TestWrapper: + def __init__(self, dstclass, filename, dst=None): + self._ignored_exceptions = [] + self._dstclass = dstclass + self._filename = filename + self._make_reload = False + self._dst = dst + self.open() + + def pass_exception_type(self, et): + self._ignored_exceptions.append(et) + + def nopass_exception_type(self, et): + self._ignored_exceptions.remove(et) + + def make_reload(self): + self._make_reload = True + + def open(self): + if self._dst: + self._dst.load_mmap(self._filename) + else: + self._dst = self._dstclass(self._filename) + + def close(self): + self._dst.save_mmap(self._filename) + + def do(self, function, *args, **kwargs): + if self._make_reload: + try: + self.open() + except Exception as e: + raise TestCrashError(get_tb(), e, "[Loading]") + + try: + fn = getattr(self._dst, function) + except KeyError: + raise TestInternalError("Model lacks function `%s'" % function) + + try: + ret = fn(*args, **kwargs) + except Exception as e: + if type(e) in self._ignored_exceptions: + raise e + details = str(args) + str(kwargs) + for arg in args: + if isinstance(arg, chirp_common.Memory): + details += os.linesep + \ + os.linesep.join(["%s:%s" % (k, v) for k, v + in list(arg.__dict__.items())]) + raise TestCrashError(get_tb(), e, details) + + if self._make_reload: + try: + self.close() + except Exception as e: + raise TestCrashError(get_tb(), e, "[Saving]") + + return ret + + def get_id(self): + return "%s %s %s" % (self._dst.VENDOR, + self._dst.MODEL, + self._dst.VARIANT) + + def get_radio(self): + return self._dst + + +class TestCase: + def __init__(self, wrapper): + self._wrapper = wrapper + + def prepare(self): + pass + + def run(self): + "Return True or False for Pass/Fail" + pass + + def cleanup(self): + pass + + def compare_mem(self, a, b, ignore=None): + rf = self._wrapper.do("get_features") + + if a.tmode == "Cross": + tx_mode, rx_mode = a.cross_mode.split("->") + + for k, v in list(a.__dict__.items()): + if ignore and k in ignore: + continue + if k == "power": + continue # FIXME + elif k == "immutable": + continue + elif k == "name": + if not rf.has_name: + continue # Don't complain about name, if not supported + else: + # Name mismatch fair if filter_name() is right + v = self._wrapper.do("filter_name", v).rstrip() + elif k == "tuning_step" and not rf.has_tuning_step: + continue + elif k == "rtone" and not ( + a.tmode == "Tone" or + (a.tmode == "TSQL" and not rf.has_ctone) or + (a.tmode == "Cross" and tx_mode == "Tone") or + (a.tmode == "Cross" and rx_mode == "Tone" and + not rf.has_ctone) + ): + continue + elif k == "ctone" and (not rf.has_ctone or + not (a.tmode == "TSQL" or + (a.tmode == "Cross" and + rx_mode == "Tone"))): + continue + elif k == "dtcs" and not ( + (a.tmode == "DTCS" and not rf.has_rx_dtcs) or + (a.tmode == "Cross" and tx_mode == "DTCS") or + (a.tmode == "Cross" and rx_mode == "DTCS" and + not rf.has_rx_dtcs)): + continue + elif k == "rx_dtcs" and (not rf.has_rx_dtcs or + not (a.tmode == "Cross" and + rx_mode == "DTCS")): + continue + elif k == "offset" and not a.duplex: + continue + elif k == "cross_mode" and a.tmode != "Cross": + continue + + try: + if b.__dict__[k] != v: + msg = "Field `%s' " % k + \ + "is `%s', " % b.__dict__[k] + \ + "expected `%s' " % v + # If we set a channel that came back with a duplex + # of 'off', we may have been outside the transmit range of + # the radio, so we should not fail. + if k == "duplex" and b.__dict__[k] == "off": + continue + details = msg + details += os.linesep + "### Wanted:" + os.linesep + details += os.linesep.join(["%s:%s" % (k, v) for k, v + in list(a.__dict__.items())]) + details += os.linesep + "### Got:" + os.linesep + details += os.linesep.join(["%s:%s" % (k, v) for k, v + in list(b.__dict__.items())]) + raise TestFailedError(msg, details) + except KeyError as e: + print(sorted(a.__dict__.keys())) + print(sorted(b.__dict__.keys())) + raise + + +class TestCaseCopyAll(TestCase): + "Copy Memories From CSV" + + def __str__(self): + return "CopyAll" + + def prepare(self): + testbase = os.path.dirname(os.path.abspath(__file__)) + source = os.path.join(testbase, 'images', 'Generic_CSV.csv') + self._src = generic_csv.CSVRadio(source) + + def run(self): + src_rf = self._src.get_features() + bounds = src_rf.memory_bounds + + dst_rf = self._wrapper.do("get_features") + dst_number = dst_rf.memory_bounds[0] + + failures = [] + + for number in range(bounds[0], bounds[1]): + src_mem = self._src.get_memory(number) + if src_mem.empty: + continue + + try: + dst_mem = import_logic.import_mem(self._wrapper.get_radio(), + src_rf, src_mem, + overrides={ + "number": dst_number}) + import_logic.import_bank(self._wrapper.get_radio(), + self._src, + dst_mem, + src_mem) + except import_logic.DestNotCompatible: + continue + except import_logic.ImportError as e: + failures.append(TestFailedError("<%i>: Import Failed: %s" % + (dst_number, e))) + continue + except Exception as e: + raise TestCrashError(get_tb(), e, "[Import]") + + self._wrapper.do("set_memory", dst_mem) + ret_mem = self._wrapper.do("get_memory", dst_number) + + try: + self.compare_mem(dst_mem, ret_mem) + except TestFailedError as e: + failures.append( + TestFailedError("<%i>: %s" % (number, e), e.get_detail())) + + return failures +TESTS["CopyAll"] = TestCaseCopyAll + + +class TestCaseBruteForce(TestCase): + def __str__(self): + return "BruteForce" + + def set_and_compare(self, m): + msgs = self._wrapper.do("validate_memory", m) + if msgs: + # If the radio correctly refuses memories it can't + # store, don't fail + return + + self._wrapper.do("set_memory", m) + ret_m = self._wrapper.do("get_memory", m.number) + + # Damned Baofeng radios don't seem to properly store + # shift and direction, so be gracious here + if m.duplex == "split" and ret_m.duplex in ["-", "+"]: + ret_m.offset = ret_m.freq + \ + (ret_m.offset * int(ret_m.duplex + "1")) + ret_m.duplex = "split" + + self.compare_mem(m, ret_m) + + def do_tone(self, m, rf): + self._wrapper.pass_exception_type(errors.UnsupportedToneError) + for tone in chirp_common.TONES: + for tmode in rf.valid_tmodes: + if tmode not in chirp_common.TONE_MODES: + continue + elif tmode in ["DTCS", "DTCS-R", "Cross"]: + continue # We'll test DCS and Cross tones separately + + m.tmode = tmode + if tmode == "": + pass + elif tmode == "Tone": + m.rtone = tone + elif tmode in ["TSQL", "TSQL-R"]: + if rf.has_ctone: + m.ctone = tone + else: + m.rtone = tone + else: + raise TestInternalError("Unknown tone mode `%s'" % tmode) + + try: + self.set_and_compare(m) + except errors.UnsupportedToneError as e: + # If a radio doesn't support a particular tone value, + # don't punish it + pass + self._wrapper.nopass_exception_type(errors.UnsupportedToneError) + + def do_dtcs(self, m, rf): + if not rf.has_dtcs: + return + + m.tmode = "DTCS" + for code in rf.valid_dtcs_codes: + m.dtcs = code + self.set_and_compare(m) + + if not rf.has_dtcs_polarity: + return + + for pol in rf.valid_dtcs_pols: + m.dtcs_polarity = pol + self.set_and_compare(m) + + def do_cross(self, m, rf): + if not rf.has_cross: + return + + m.tmode = "Cross" + # No fair asking a radio to detect two identical tones as Cross instead + # of TSQL + m.rtone = 100.0 + m.ctone = 107.2 + m.dtcs = 506 + m.rx_dtcs = 516 + for cross_mode in rf.valid_cross_modes: + m.cross_mode = cross_mode + self.set_and_compare(m) + + def do_duplex(self, m, rf): + for duplex in rf.valid_duplexes: + if duplex not in ["", "-", "+", "split"]: + continue + if duplex == "split" and not rf.can_odd_split: + raise TestFailedError("Forgot to set rf.can_odd_split!") + if duplex == "split": + m.offset = rf.valid_bands[0][1] - 100000 + m.duplex = duplex + self.set_and_compare(m) + + if rf.can_odd_split and "split" not in rf.valid_duplexes: + raise TestFailedError("Paste error: rf.can_odd_split defined, but " + "split duplex not supported.") + + def do_skip(self, m, rf): + for skip in rf.valid_skips: + m.skip = skip + self.set_and_compare(m) + + def do_mode(self, m, rf): + def ensure_urcall(call): + l = self._wrapper.do("get_urcall_list") + l[0] = call + self._wrapper.do("set_urcall_list", l) + + def ensure_rptcall(call): + l = self._wrapper.do("get_repeater_call_list") + l[0] = call + self._wrapper.do("set_repeater_call_list", l) + + def freq_is_ok(freq): + for lo, hi in rf.valid_bands: + if freq > lo and freq < hi: + return True + return False + + successes = 0 + for mode in rf.valid_modes: + tmp = copy.deepcopy(m) + if mode not in chirp_common.MODES: + continue + if mode == "DV": + tmp = chirp_common.DVMemory() + try: + ensure_urcall(tmp.dv_urcall) + ensure_rptcall(tmp.dv_rpt1call) + ensure_rptcall(tmp.dv_rpt2call) + except IndexError: + if rf.requires_call_lists: + raise + else: + # This radio may not do call lists at all, + # so let it slide + pass + if mode == "FM" and freq_is_ok(tmp.freq + 100000000): + # Some radios don't support FM below approximately 30MHz, + # so jump up by 100MHz, if they support that + tmp.freq += 100000000 + + tmp.mode = mode + + if rf.validate_memory(tmp): + # A result (of error messages) from validate means the radio + # thinks this is invalid, so don't fail the test + print('Failed to validate %s: %s' % (tmp, rf.validate_memory(tmp))) + continue + + self.set_and_compare(tmp) + successes += 1 + + if (not successes) and rf.valid_modes: + raise TestFailedError("All modes were skipped, " + "something went wrong") + + def run(self): + rf = self._wrapper.do("get_features") + + def clean_mem(): + m = chirp_common.Memory() + m.number = rf.memory_bounds[0] + try: + m.mode = rf.valid_modes[0] + except IndexError: + pass + if rf.valid_bands: + m.freq = rf.valid_bands[0][0] + 600000 + else: + m.freq = 146520000 + if m.freq < 30000000 and "AM" in rf.valid_modes: + m.mode = "AM" + return m + + tests = [ + self.do_tone, + self.do_dtcs, + self.do_cross, + self.do_duplex, + self.do_skip, + self.do_mode, + ] + for test in tests: + test(clean_mem(), rf) + + if 12.5 in rf.valid_tuning_steps and \ + "split" in rf.valid_duplexes: + m = clean_mem() + if rf.valid_bands: + m.offset = rf.valid_bands[0][1] - 12500 + else: + m.offset = 151137500 + m.duplex = "split" + self.set_and_compare(m) + + return [] +TESTS["BruteForce"] = TestCaseBruteForce + + +class TestCaseEdges(TestCase): + def __str__(self): + return "Edges" + + def _mem(self, rf): + m = chirp_common.Memory() + m.freq = rf.valid_bands[0][0] + 1000000 + if m.freq < 30000000 and "AM" in rf.valid_modes: + m.mode = "AM" + else: + try: + m.mode = rf.valid_modes[0] + except IndexError: + pass + + for i in range(*rf.memory_bounds): + m.number = i + if not self._wrapper.do("validate_memory", m): + return m + + raise TestSkippedError("No mutable memory locations found") + + def do_longname(self, rf): + m = self._mem(rf) + m.name = ("X" * 256) # Should be longer than any radio can handle + m.name = self._wrapper.do("filter_name", m.name) + + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + + self.compare_mem(m, n) + + def do_badname(self, rf): + m = self._mem(rf) + + ascii = "".join([chr(x) for x in range(ord(" "), ord("~")+1)]) + for i in range(0, len(ascii), 4): + m.name = self._wrapper.do("filter_name", ascii[i:i+4]) + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + self.compare_mem(m, n) + + def do_bandedges(self, rf): + m = self._mem(rf) + min_step = min(rf.has_tuning_step and rf.valid_tuning_steps or [10]) + + for low, high in rf.valid_bands: + for freq in (low, high - int(min_step * 1000)): + m.freq = freq + if self._wrapper.do("validate_memory", m): + # Radio doesn't like it, so skip + continue + + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + self.compare_mem(m, n) + + def do_oddsteps(self, rf): + odd_steps = { + 145000000: [145856250, 145862500], + 445000000: [445856250, 445862500], + 862000000: [862731250, 862737500], + } + + m = self._mem(rf) + + for low, high in rf.valid_bands: + for band, totest in list(odd_steps.items()): + if band < low or band > high: + continue + for testfreq in totest: + step = chirp_common.required_step(testfreq) + if step not in rf.valid_tuning_steps: + continue + + m.freq = testfreq + m.tuning_step = step + self._wrapper.do("set_memory", m) + n = self._wrapper.do("get_memory", m.number) + self.compare_mem(m, n, ignore=['tuning_step']) + + def do_empty_to_not(self, rf): + firstband = rf.valid_bands[0] + testfreq = firstband[0] + for loc in range(*rf.memory_bounds): + m = self._wrapper.do('get_memory', loc) + if m.empty: + m.empty = False + m.freq = testfreq + self._wrapper.do('set_memory', m) + m = self._wrapper.do('get_memory', loc) + if m.freq == testfreq: + return + else: + raise TestFailedError('Radio failed to set an empty ' + 'location (%i)' % loc) + + def do_delete_memory(self, rf): + firstband = rf.valid_bands[0] + testfreq = firstband[0] + for loc in range(*rf.memory_bounds): + if loc == rf.memory_bounds[0]: + # Some radios will not allow you to delete the first memory + # /me glares at yaesu + continue + m = self._wrapper.do('get_memory', loc) + if not m.empty: + m.empty = True + self._wrapper.do('set_memory', m) + m = self._wrapper.do('get_memory', loc) + if not m.empty: + raise TestFailedError('Radio refused to delete a memory ' + 'location (%i)' % loc) + else: + return + + def run(self): + rf = self._wrapper.do("get_features") + + if not rf.valid_bands: + raise TestFailedError("Radio does not provide valid bands!") + + self.do_longname(rf) + self.do_bandedges(rf) + self.do_oddsteps(rf) + self.do_badname(rf) + if rf.can_delete: + self.do_empty_to_not(rf) + self.do_delete_memory(rf) + + return [] + +TESTS["Edges"] = TestCaseEdges + + +class TestCaseSettings(TestCase): + def __str__(self): + return "Settings" + + def do_get_settings(self, rf): + lst = self._wrapper.do("get_settings") + if not isinstance(lst, list): + raise TestFailedError("Invalid Radio Settings") + + def do_same_settings(self, rf): + o = self._wrapper.do("get_settings") + self._wrapper.do("set_settings", o) + n = self._wrapper.do("get_settings") + list(map(self.compare_settings, o, n)) + + @staticmethod + def compare_settings(a, b): + try: + if isinstance(a, settings.RadioSettingValue): + raise TypeError('Hit bottom') + list(map(TestCaseSettings.compare_settings, a, b)) + except TypeError: + if a.get_value() != b.get_value(): + msg = "Field is `%s', " % b + \ + "expected `%s' " % a + details = msg + raise TestFailedError(msg, details) + + def run(self): + rf = self._wrapper.do("get_features") + + if not rf.has_settings: + raise TestSkippedError("Settings not supported") + + self.do_get_settings(rf) + self.do_same_settings(rf) + + return [] + +TESTS["Settings"] = TestCaseSettings + + +class TestCaseBanks(TestCase): + def __str__(self): + return "Banks" + + def _do_bank_names(self, rf, testname): + bm = self._wrapper.do("get_bank_model") + banks = bm.get_mappings() + + for bank in banks: + name = bank.get_name() + try: + bank.set_name(testname) + except AttributeError: + return [], [] + except Exception as e: + if str(e) == "Not implemented": + return [], [] + else: + raise e + + return banks, bm.get_mappings() + + def do_bank_names(self, rf): + banks, newbanks = self._do_bank_names(rf, "T") + + for i in range(0, len(banks)): + if banks[i].get_name() != newbanks[i].get_name(): + raise TestFailedError("Bank names not preserved", + "Tried %s on %i\nGot %s" % (banks[i], + i, + newbanks[i])) + + def do_bank_names_toolong(self, rf): + testname = "Not possibly this long" + banks, newbanks = self._do_bank_names(rf, testname) + + for i in range(0, len(newbanks)): + # Truncation is allowed, but not failure + if not testname.lower().startswith(str(newbanks[i]).lower()): + raise TestFailedError("Bank names not properly truncated", + "Tried: %s on %i\nGot: %s" % + (testname, i, newbanks[i])) + + def do_bank_names_no_trailing_whitespace(self, rf): + banks, newbanks = self._do_bank_names(rf, "foo ") + + for bank in newbanks: + if str(bank) != str(bank).rstrip(): + raise TestFailedError("Bank names stored with " + + "trailing whitespace") + + def do_bank_store(self, rf): + loc = rf.memory_bounds[0] + mem = chirp_common.Memory() + mem.number = loc + mem.freq = rf.valid_bands[0][0] + 100000 + + # Make sure the memory is empty and we create it from scratch + mem.empty = True + self._wrapper.do("set_memory", mem) + + mem.empty = False + self._wrapper.do("set_memory", mem) + + model = self._wrapper.do("get_bank_model") + + # If in your bank model every channel has to be tied to a bank, just + # add a variable named channelAlwaysHasBank to it and make it True + try: + channelAlwaysHasBank = model.channelAlwaysHasBank + except: + channelAlwaysHasBank = False + + mem_banks = model.get_memory_mappings(mem) + if channelAlwaysHasBank: + if len(mem_banks) == 0: + raise TestFailedError("Freshly-created memory has no banks " + + "and it should", "Bank: %s" % + str(mem_banks)) + else: + if len(mem_banks) != 0: + raise TestFailedError("Freshly-created memory has banks " + + "and should not", "Bank: %s" % + str(mem_banks)) + + banks = model.get_mappings() + + def verify(bank): + if bank not in model.get_memory_mappings(mem): + return "Memory does not claim bank" + + if loc not in [x.number for x in model.get_mapping_memories(bank)]: + return "Bank does not claim memory" + + return None + + model.add_memory_to_mapping(mem, banks[0]) + reason = verify(banks[0]) + if reason is not None: + raise TestFailedError("Setting memory bank does not persist", + "%s\nMemory banks:%s\nBank memories:%s" % + (reason, + model.get_memory_mappings(mem), + model.get_mapping_memories(banks[0]))) + + model.remove_memory_from_mapping(mem, banks[0]) + reason = verify(banks[0]) + if reason is None and not channelAlwaysHasBank: + raise TestFailedError("Memory remains in bank after remove", + reason) + + try: + model.remove_memory_from_mapping(mem, banks[0]) + did_error = False + except Exception: + did_error = True + + if not did_error and not channelAlwaysHasBank: + raise TestFailedError("Removing memory from non-member bank " + + "did not raise Exception") + + def do_bank_index(self, rf): + if not rf.has_bank_index: + return + + loc = rf.memory_bounds[0] + mem = chirp_common.Memory() + mem.number = loc + mem.freq = rf.valid_bands[0][0] + 100000 + + self._wrapper.do("set_memory", mem) + + model = self._wrapper.do("get_bank_model") + banks = model.get_mappings() + index_bounds = model.get_index_bounds() + + model.add_memory_to_mapping(mem, banks[0]) + for i in range(0, *index_bounds): + model.set_memory_index(mem, banks[0], i) + if model.get_memory_index(mem, banks[0]) != i: + raise TestFailedError("Bank index not persisted") + + suggested_index = model.get_next_mapping_index(banks[0]) + if suggested_index not in list(range(*index_bounds)): + raise TestFailedError("Suggested bank index not in valid range", + "Got %i, range is %s" % (suggested_index, + index_bounds)) + + def run(self): + rf = self._wrapper.do("get_features") + + if not rf.has_bank: + raise TestSkippedError("Banks not supported") + + self.do_bank_names(rf) + self.do_bank_names_toolong(rf) + self.do_bank_names_no_trailing_whitespace(rf) + self.do_bank_store(rf) + # Again to make sure we clear bank info on delete + self.do_bank_store(rf) + self.do_bank_index(rf) + + return [] + +TESTS["Banks"] = TestCaseBanks + + +class TestCaseDetect(TestCase): + def __str__(self): + return "Detect" + + def run(self): + if isinstance(self._wrapper._dst, chirp_common.LiveRadio): + raise TestSkippedError("This is a live radio") + + filename = self._wrapper._filename + + try: + radio = directory.get_radio_by_image(filename) + except Exception as e: + raise TestFailedError("Failed to detect", str(e)) + + if radio.__class__.__name__ == 'DynamicRadioAlias': + # This was detected via metadata and wrapped, which means + # we found the appropriate class. + pass + elif issubclass(self._wrapper._dstclass, radio.__class__): + pass + elif issubclass(radio.__class__, self._wrapper._dstclass): + pass + elif radio.__class__ != self._wrapper._dstclass: + raise TestFailedError("%s detected as %s" % + (self._wrapper._dstclass, radio.__class__)) + return [] + +TESTS["Detect"] = TestCaseDetect + + +class TestCaseClone(TestCase): + class SerialNone: + def __init__(self, isbytes): + self.isbytes = isbytes + self.mismatch = False + self.mismatch_at = None + + def read(self, size): + if self.isbytes: + return b"" + else: + return "" + + def write(self, data): + expected = self.isbytes and bytes or str + if six.PY2: + # One driver uses bytearray() which will trigger this + # check even though it works fine on py2. So, only + # do this check for PY3 which is where it matters + # anyway. + pass + elif not self.mismatch and not isinstance(data, expected): + self.mismatch = True + self.mismatch_at = ''.join(traceback.format_stack()) + pass + + def setBaudrate(self, rate): + pass + + def setTimeout(self, timeout): + pass + + def setParity(self, parity): + pass + + def __str__(self): + return self.__class__.__name__.replace("Serial", "") + + class SerialError(SerialNone): + def read(self, size): + raise Exception("Foo") + + def write(self, data): + raise Exception("Bar") + + class SerialGarbage(SerialNone): + def read(self, size): + if self.isbytes: + buf = [] + for i in range(0, size): + buf += i % 256 + return bytes(buf) + else: + buf = "" + for i in range(0, size): + buf += chr(i % 256) + return buf + + class SerialShortGarbage(SerialNone): + def read(self, size): + if self.isbytes: + return b'\x00' * (size - 1) + else: + return "\x00" * (size - 1) + + def __str__(self): + return "Clone" + + def _run(self, serial): + error = None + live = isinstance(self._wrapper._dst, chirp_common.LiveRadio) + clone = isinstance(self._wrapper._dst, chirp_common.CloneModeRadio) + + if not clone and not live: + raise TestSkippedError('Does not support clone') + + try: + radio = self._wrapper._dst.__class__(serial) + radio.status_fn = lambda s: True + except Exception as e: + error = e + + if not live: + if error is not None: + raise TestFailedError("Clone radio tried to read from " + + "serial on init") + else: + if not isinstance(error, errors.RadioError): + raise TestFailedError("Live radio didn't notice serial " + + "was dead on init") + return [] # Nothing more to test on an error'd live radio + + error = None + try: + radio.sync_in() + except Exception as e: + error = e + + if error is None: + raise TestFailedError("Radio did not raise exception " + + "with %s data" % serial, + "On sync_in()") + elif not isinstance(error, errors.RadioError): + raise TestFailedError("Radio did not raise RadioError " + + "with %s data" % serial, + "sync_in() Got: %s (%s)\n%s" % + (error.__class__.__name__, + error, get_tb())) + + if radio.NEEDS_COMPAT_SERIAL: + radio._mmap = memmap.MemoryMap("\x00" * (1024 * 128)) + else: + radio._mmap = memmap.MemoryMapBytes(bytes(b"\x00") * (1024 * 128)) + + error = None + try: + radio.sync_out() + except Exception as e: + error = e + + if error is None: + raise TestFailedError("Radio did not raise exception " + + "with %s data" % serial, + "On sync_out()") + elif not isinstance(error, errors.RadioError): + raise TestFailedError("Radio did not raise RadioError " + + "with %s data" % serial, + "sync_out(): Got: %s (%s)" % + (error.__class__.__name__, error)) + + if serial.mismatch: + raise TestFailedError("Radio tried to write the wrong " + "type of data to the %s pipe." % ( + serial.__class__.__name__), + "TestClone:%s\n%s" % ( + serial.__class__.__name__, + serial.mismatch_at)) + + return [] + + def run(self): + isbytes = not self._wrapper._dst.NEEDS_COMPAT_SERIAL + self._run(self.SerialError(isbytes)) + self._run(self.SerialNone(isbytes)) + self._run(self.SerialGarbage(isbytes)) + self._run(self.SerialShortGarbage(isbytes)) + return [] + +TESTS["Clone"] = TestCaseClone + + +class TestOutput: + def __init__(self, output=None): + if not output: + output = sys.stdout + self._out = output + + def prepare(self): + pass + + def cleanup(self): + pass + + def _print(self, string): + print(string, file=self._out) + + def report(self, rclass, tc, msg, e): + name = ("%s %s" % (rclass.MODEL, rclass.VARIANT))[:13] + self._print("%9s %-13s %-10s %s %s" % (rclass.VENDOR.split(" ")[0], + name, + tc, + msg, e)) + + +class TestOutputANSI(TestOutput): + def __init__(self, output=None): + TestOutput.__init__(self, output) + self.__counts = { + "PASSED": 0, + "FAILED": 0, + "CRASHED": 0, + "SKIPPED": 0, + } + self.__total = 0 + + def report(self, rclass, tc, msg, e): + self.__total += 1 + self.__counts[msg] += 1 + msg += ":" + if os.isatty(1): + if msg == "PASSED:": + msg = "\033[1;32m%8s\033[0m" % msg + elif msg == "FAILED:": + msg = "\033[1;41m%8s\033[0m" % msg + elif msg == "CRASHED:": + msg = "\033[1;45m%8s\033[0m" % msg + elif msg == "SKIPPED:": + msg = "\033[1;32m%8s\033[0m" % msg + else: + msg = "%8s" % msg + + TestOutput.report(self, rclass, tc, msg, e) + + def cleanup(self): + self._print("-" * 70) + self._print("Results:") + self._print(" %-7s: %i" % ("TOTAL", self.__total)) + for t, c in list(self.__counts.items()): + self._print(" %-7s: %i" % (t, c)) + + +class TestOutputHTML(TestOutput): + def __init__(self, filename): + self._filename = filename + + def prepare(self): + print("Writing to %s" % self._filename, end=' ') + sys.stdout.flush() + self._out = file(self._filename, "w") + s = """ + + +Test report for CHIRP version %s + + + +

Test report for CHIRP version %s

+

Generated on %s (%s)

+ + + + + +""" % (CHIRP_VERSION, CHIRP_VERSION, time.strftime("%x at %X"), os.name) + print(s, file=self._out) + + def cleanup(self): + print("
VendorModelTest CaseStatusMessage
", file=self._out) + self._out.close() + print("Done") + + def report(self, rclass, tc, msg, e): + s = ("" % msg) + \ + ("%s" % rclass.VENDOR) + \ + ("%s %s" % + (rclass.MODEL, rclass.VARIANT)) + \ + ("%s" % tc) + \ + ("%s" % (msg, msg)) + \ + ("%s" % e) + \ + "" + print(s, file=self._out) + sys.stdout.write(".") + sys.stdout.flush() + + +class TestRunner: + def __init__(self, images_dir, test_list, test_out): + self._images_dir = images_dir + self._test_list = test_list + self._test_out = test_out + if not os.path.exists("tmp"): + os.mkdir("tmp") + + def _make_list(self): + run_list = [] + images = glob.glob(os.path.join(self._images_dir, "*.img")) + for image in sorted(images): + drv_name, _ = os.path.splitext(os.path.basename(image)) + run_list.append((directory.get_radio(drv_name), image)) + return run_list + + def report(self, rclass, tc, msg, e): + self._test_out.report(rclass, tc, msg, e) + + def log(self, rclass, tc, e): + fn = "logs/%s_%s.log" % (directory.radio_class_id(rclass), tc) + log = file(fn, "a") + print("---- Begin test %s ----" % tc, file=log) + log.write(e.get_detail()) + print(file=log) + print("---- End test %s ----" % tc, file=log) + log.close() + + def nuke_log(self, rclass, tc): + fn = "logs/%s_%s.log" % (directory.radio_class_id(rclass), tc) + if os.path.exists(fn): + os.remove(fn) + + def _run_one(self, rclass, parm, dst=None): + nfailed = 0 + for tcclass in self._test_list: + nprinted = 0 + tw = TestWrapper(rclass, parm, dst=dst) + tc = tcclass(tw) + + self.nuke_log(rclass, tc) + + tc.prepare() + + try: + failures = tc.run() + for e in failures: + self.report(rclass, tc, "FAILED", e) + if e.get_detail(): + self.log(rclass, tc, e) + nfailed += 1 + nprinted += 1 + except TestFailedError as e: + self.report(rclass, tc, "FAILED", e) + if e.get_detail(): + self.log(rclass, tc, e) + nfailed += 1 + nprinted += 1 + except TestCrashError as e: + self.report(rclass, tc, "CRASHED", e) + self.log(rclass, tc, e) + nfailed += 1 + nprinted += 1 + except TestSkippedError as e: + self.report(rclass, tc, "SKIPPED", e) + self.log(rclass, tc, e) + nprinted += 1 + + tc.cleanup() + + if not nprinted: + self.report(rclass, tc, "PASSED", "All tests") + + return nfailed + + def run_rclass_image(self, rclass, image, dst=None): + rid = "%s_%s_" % (rclass.VENDOR, rclass.MODEL) + rid = rid.replace("/", "_") + # Do this for things like Generic_CSV, that demand it + _base, ext = os.path.splitext(image) + testimage = tempfile.mktemp(ext, rid) + shutil.copy(image, testimage) + + try: + tw = TestWrapper(rclass, testimage, dst=dst) + rf = tw.do("get_features") + if rf.has_sub_devices: + devices = tw.do("get_sub_devices") + failed = 0 + for dev in devices: + failed += self.run_rclass_image(dev.__class__, image, dst=dev) + return failed + else: + return self._run_one(rclass, image, dst=dst) + finally: + os.remove(testimage) + + def run_list(self, run_list): + def _key(pair): + return pair[0].VENDOR + pair[0].MODEL + pair[0].VARIANT + failed = 0 + for rclass, image in sorted(run_list, key=_key): + failed += self.run_rclass_image(rclass, image) + return failed + + def run_all(self): + run_list = self._make_list() + return self.run_list(run_list) + + def run_one(self, drv_name): + return self.run_rclass_image(directory.get_radio(drv_name), + os.path.join("images", + "%s.img" % drv_name)) + + def run_one_live(self, drv_name, port): + rclass = directory.get_radio(drv_name) + pipe = Serial(port=port, baudrate=rclass.BAUD_RATE, timeout=0.5) + tw = TestWrapper(rclass, pipe) + rf = tw.do("get_features") + if rf.has_sub_devices: + devices = tw.do("get_sub_devices") + failed = 0 + for device in devices: + failed += self._run_one(device.__class__, pipe) + return failed + else: + return self._run_one(rclass, pipe) + +if __name__ == "__main__": + import sys + + images = glob.glob("images/*.img") + tests = [os.path.splitext(os.path.basename(img))[0] for img in images] + + op = OptionParser() + op.add_option("-d", "--driver", dest="driver", default=None, + help="Driver to test (omit for all)") + op.add_option("-t", "--test", dest="test", default=None, + help="Test to run (omit for all)") + op.add_option("-e", "--exclude", dest="exclude", default=None, + help="Test to exclude") + op.add_option("", "--html", dest="html", default=None, + help="Output to HTML file") + op.add_option("-l", "--live", dest="live", default=None, + help="Live radio on this port (requires -d)") + op.usage = """ +Available drivers: +%s +Available tests: +%s +""" % ("\n".join([" %s" % x for x in sorted(tests)]), + "\n".join([" %s" % x for x in sorted(TESTS.keys())])) + + (options, args) = op.parse_args() + + if options.html: + test_out = TestOutputHTML(options.html) + else: + stdout = sys.stdout + if not os.path.exists("logs"): + os.mkdir("logs") + sys.stdout = file("logs/verbose", "w") + test_out = TestOutputANSI(stdout) + + test_out.prepare() + + if options.exclude: + del TESTS[options.exclude] + + if options.test: + tr = TestRunner("images", [TESTS[options.test]], test_out) + else: + tr = TestRunner("images", list(TESTS.values()), test_out) + + if options.live: + if not options.driver: + print("Live mode requires a driver to be specified") + sys.exit(1) + failed = tr.run_one_live(options.driver, options.live) + elif options.driver: + failed = tr.run_one(options.driver) + else: + failed = tr.run_all() + + test_out.cleanup() + + sys.exit(failed) diff --git a/tests/test_drivers.py b/tests/test_drivers.py new file mode 100644 index 0000000..ba8aaf9 --- /dev/null +++ b/tests/test_drivers.py @@ -0,0 +1,24 @@ +import sys +import unittest + +from chirp import directory +from tests import load_tests + + +class TestSuiteAdapter(object): + """Adapter for pytest since it doesn't support the loadTests() protocol""" + + def __init__(self, locals): + self.locals = locals + + def loadTestsFromTestCase(self, test_cls): + self.locals[test_cls.__name__] = test_cls + + @staticmethod + def addTests(tests): + pass + + +directory.safe_import_drivers() +adapter = TestSuiteAdapter(locals()) +load_tests(adapter, None, None, suite=adapter) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/base.py b/tests/unit/base.py new file mode 100644 index 0000000..1e112ce --- /dev/null +++ b/tests/unit/base.py @@ -0,0 +1,50 @@ +import sys +import unittest + +import mock + +try: + import mox +except ImportError: + from mox3 import mox + +import warnings +warnings.simplefilter('ignore', Warning) + + +class BaseTest(unittest.TestCase): + def setUp(self): + __builtins__['_'] = lambda s: s + self.mox = mox.Mox() + + def tearDown(self): + self.mox.UnsetStubs() + self.mox.VerifyAll() + + +pygtk_mocks = ('gtk', 'pango', 'gobject') +pygtk_base_classes = ('gobject.GObject', 'gtk.HBox', 'gtk.Dialog') + + +class DummyBase(object): + def __init__(self, *a, **k): + # gtk.Dialog + self.vbox = mock.MagicMock() + + # gtk.Dialog + def set_position(self, pos): + pass + + +def mock_gtk(): + for module in pygtk_mocks: + sys.modules[module] = mock.MagicMock() + + for path in pygtk_base_classes: + module, base_class = path.split('.') + setattr(sys.modules[module], base_class, DummyBase) + + +def unmock_gtk(): + for module in pygtk_mocks: + del sys.modules[module] diff --git a/tests/unit/test_bitwise.py b/tests/unit/test_bitwise.py new file mode 100644 index 0000000..2b674e8 --- /dev/null +++ b/tests/unit/test_bitwise.py @@ -0,0 +1,341 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import struct +import unittest + +import six + +from chirp import bitwise +from chirp import memmap + + +class BaseTest(unittest.TestCase): + def _compare_structure(self, obj, primitive): + for key, value in primitive.items(): + if isinstance(value, dict): + self._compare_structure(getattr(obj, key), value) + else: + self.assertEqual(type(value)(getattr(obj, key)), value) + + +class TestMemoryMapCoherence(BaseTest): + def test_byte_char_coherence(self): + charmmap = memmap.MemoryMap('00') + # This will to a get_byte_compatible() from chars + obj = bitwise.parse('char foo[2];', charmmap) + self.assertEqual('00', str(obj.foo)) + obj.foo = '11' + # The above assignment happens on the byte-compatible mmap, + # make sure it is still visible in the charmmap we know about. + # This confirms that get_byte_compatible() links the backing + # store of the original mmap to the new one. + self.assertEqual('11', charmmap.get_packed()) + + +class TestBitwiseBaseIntTypes(BaseTest): + def _test_type(self, datatype, _data, value): + data = memmap.MemoryMapBytes(bytes(_data)) + obj = bitwise.parse("%s foo;" % datatype, data) + self.assertEqual(int(obj.foo), value) + self.assertEqual(obj.foo.size(), len(data) * 8) + + obj.foo = 0 + self.assertEqual(int(obj.foo), 0) + self.assertEqual(data.get_packed(), (b"\x00" * (obj.size() // 8))) + + obj.foo = value + self.assertEqual(int(obj.foo), value) + self.assertEqual(data.get_packed(), _data) + + obj.foo = 7 + # Compare against the equivalent real division so we get consistent + # results on py2 and py3 + self.assertEqual(7 // 2, obj.foo // 2) + self.assertEqual(7 / 2, obj.foo / 2) + self.assertEqual(7 / 2.0, obj.foo / 2.0) + + def test_type_u8(self): + self._test_type("u8", b"\x80", 128) + + def test_type_u16(self): + self._test_type("u16", b"\x01\x00", 256) + + def test_type_u24(self): + self._test_type("u24", b"\x80\x00\x00", 2**23) + + def test_type_u32(self): + self._test_type("u32", b"\x80\x00\x00\x00", 2**31) + + def test_type_ul16(self): + self._test_type("ul16", b"\x00\x01", 256) + + def test_type_ul24(self): + self._test_type("ul24", b"\x00\x00\x80", 2**23) + + def test_type_ul32(self): + self._test_type("ul32", b"\x00\x00\x00\x80", 2**31) + + def test_int_array(self): + data = memmap.MemoryMapBytes(bytes(b'\x00\x01\x02\x03')) + obj = bitwise.parse('u8 foo[4];', data) + for i in range(4): + self.assertEqual(i, obj.foo[i]) + obj.foo[i] = i * 2 + self.assertEqual(b'\x00\x02\x04\x06', data.get_packed()) + + def test_int_array(self): + data = memmap.MemoryMapBytes(bytes(b'\x00\x01\x02\x03')) + obj = bitwise.parse('u8 foo[4];', data) + for i in range(4): + self.assertEqual(i, obj.foo[i]) + obj.foo[i] = i * 2 + self.assertEqual(b'\x00\x02\x04\x06', data.get_packed()) + + +class TestBitfieldTypes(BaseTest): + def test_bitfield_u8(self): + defn = "u8 foo:4, bar:4;" + data = memmap.MemoryMapBytes(bytes(b"\x12")) + obj = bitwise.parse(defn, data) + self.assertEqual(obj.foo, 1) + self.assertEqual(obj.bar, 2) + self.assertEqual(obj.foo.size(), 4) + self.assertEqual(obj.bar.size(), 4) + obj.foo = 0x8 + obj.bar = 0x1 + self.assertEqual(data.get_packed(), b"\x81") + + def _test_bitfield_16(self, variant, data): + defn = "u%s16 foo:4, bar:8, baz:4;" % variant + data = memmap.MemoryMapBytes(bytes(data)) + obj = bitwise.parse(defn, data) + self.assertEqual(int(obj.foo), 1) + self.assertEqual(int(obj.bar), 0x23) + self.assertEqual(int(obj.baz), 4) + self.assertEqual(obj.foo.size(), 4) + self.assertEqual(obj.bar.size(), 8) + self.assertEqual(obj.baz.size(), 4) + obj.foo = 0x2 + obj.bar = 0x11 + obj.baz = 0x3 + if variant == "l": + self.assertEqual(data.get_packed(), b"\x13\x21") + else: + self.assertEqual(data.get_packed(), b"\x21\x13") + + def test_bitfield_u16(self): + self._test_bitfield_16("", b"\x12\x34") + + def test_bitfield_ul16(self): + self._test_bitfield_16('l', b"\x34\x12") + + def _test_bitfield_24(self, variant, data): + defn = "u%s24 foo:12, bar:6, baz:6;" % variant + data = memmap.MemoryMapBytes(bytes(data)) + obj = bitwise.parse(defn, data) + self.assertEqual(int(obj.foo), 4) + self.assertEqual(int(obj.bar), 3) + self.assertEqual(int(obj.baz), 2) + self.assertEqual(obj.foo.size(), 12) + self.assertEqual(obj.bar.size(), 6) + self.assertEqual(obj.baz.size(), 6) + obj.foo = 1 + obj.bar = 2 + obj.baz = 3 + if variant == 'l': + self.assertEqual(data.get_packed(), b"\x83\x10\x00") + else: + self.assertEqual(data.get_packed(), b"\x00\x10\x83") + + def test_bitfield_u24(self): + self._test_bitfield_24("", b"\x00\x40\xC2") + + def test_bitfield_ul24(self): + self._test_bitfield_24("l", b"\xC2\x40\x00") + + +class TestBitType(BaseTest): + def test_bit_array(self): + defn = "bit foo[24];" + data = memmap.MemoryMapBytes(bytes(b"\x00\x80\x01")) + obj = bitwise.parse(defn, data) + for i, v in [(0, False), (8, True), (23, True)]: + self.assertEqual(bool(obj.foo[i]), v) + for i in range(0, 24): + obj.foo[i] = i % 2 + self.assertEqual(data.get_packed(), b"\x55\x55\x55") + + def test_bit_array_fail(self): + self.assertRaises(ValueError, bitwise.parse, "bit foo[23];", b"000") + + +class TestBitwiseBCDTypes(BaseTest): + def _test_def(self, definition, name, _data, value): + data = memmap.MemoryMapBytes(bytes(_data)) + obj = bitwise.parse(definition, data) + self.assertEqual(int(getattr(obj, name)), value) + self.assertEqual(getattr(obj, name).size(), len(_data) * 8) + setattr(obj, name, 0) + self.assertEqual(data.get_packed(), (b"\x00" * len(_data))) + setattr(obj, name, 42) + if definition.startswith("b"): + expected = (len(_data) == 2 and b"\x00" or b"") + b"\x42" + else: + expected = b"\x42" + (len(_data) == 2 and b"\x00" or b"") + raw = data.get_packed() + self.assertEqual(raw, expected) + + def test_bbcd(self): + self._test_def("bbcd foo;", "foo", b"\x12", 12) + + def test_lbcd(self): + self._test_def("lbcd foo;", "foo", b"\x12", 12) + + def test_bbcd_array(self): + self._test_def("bbcd foo[2];", "foo", b"\x12\x34", 1234) + + def test_lbcd_array(self): + self._test_def("lbcd foo[2];", "foo", b"\x12\x34", 3412) + + +class TestBitwiseCharTypes(BaseTest): + def test_char(self): + data = memmap.MemoryMapBytes(bytes(b"c")) + obj = bitwise.parse("char foo;", data) + self.assertEqual(str(obj.foo), "c") + self.assertEqual(obj.foo.size(), 8) + obj.foo = "d" + self.assertEqual(data.get_packed(), b"d") + + def test_string(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertEqual(str(obj.foo), "foobar") + self.assertEqual(obj.foo.size(), 8 * 6) + obj.foo = "bazfoo" + self.assertEqual(data.get_packed(), b"bazfoo") + + def test_string_invalid_chars(self): + data = memmap.MemoryMapBytes(bytes(b"\xFFoobar1")) + obj = bitwise.parse("struct {char foo[7];} bar;", data) + + if six.PY3: + expected = '\xffoobar1' + else: + expected = '\\xffoobar1' + + self.assertIn(expected, repr(obj.bar)) + + def test_string_wrong_length(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertRaises(ValueError, setattr, obj, "foo", "bazfo") + self.assertRaises(ValueError, setattr, obj, "foo", "bazfooo") + + def test_string_with_various_input_types(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertEqual('foobar', str(obj.foo)) + self.assertEqual(6, len(b'barfoo')) + obj.foo = b'barfoo' + self.assertEqual('barfoo', str(obj.foo)) + obj.foo = [ord(c) for c in 'fffbbb'] + self.assertEqual('fffbbb', str(obj.foo)) + + def test_string_get_raw(self): + data = memmap.MemoryMapBytes(bytes(b"foobar")) + obj = bitwise.parse("char foo[6];", data) + self.assertEqual('foobar', obj.foo.get_raw()) + self.assertEqual(b'foobar', obj.foo.get_raw(asbytes=True)) + + +class TestBitwiseStructTypes(BaseTest): + def _test_def(self, definition, data, primitive): + obj = bitwise.parse(definition, data) + self._compare_structure(obj, primitive) + self.assertEqual(obj.size(), len(data) * 8) + + def test_struct_one_element(self): + defn = "struct { u8 bar; } foo;" + value = {"foo": {"bar": 128}} + self._test_def(defn, b"\x80", value) + + def test_struct_two_elements(self): + defn = "struct { u8 bar; u16 baz; } foo;" + value = {"foo": {"bar": 128, "baz": 256}} + self._test_def(defn, b"\x80\x01\x00", value) + + def test_struct_writes(self): + data = memmap.MemoryMapBytes(bytes(b"..")) + defn = "struct { u8 bar; u8 baz; } foo;" + obj = bitwise.parse(defn, data) + obj.foo.bar = 0x12 + obj.foo.baz = 0x34 + self.assertEqual(data.get_packed(), b"\x12\x34") + + def test_struct_get_raw(self): + data = memmap.MemoryMapBytes(bytes(b"..")) + defn = "struct { u8 bar; u8 baz; } foo;" + obj = bitwise.parse(defn, data) + self.assertEqual('..', obj.get_raw()) + self.assertEqual(b'..', obj.get_raw(asbytes=True)) + + def test_struct_get_raw_small(self): + data = memmap.MemoryMapBytes(bytes(b".")) + defn = "struct { u8 bar; } foo;" + obj = bitwise.parse(defn, data) + self.assertEqual('.', obj.get_raw()) + self.assertEqual(b'.', obj.get_raw(asbytes=True)) + + +class TestBitwiseSeek(BaseTest): + def test_seekto(self): + defn = "#seekto 4; char foo;" + obj = bitwise.parse(defn, b"abcdZ") + self.assertEqual(str(obj.foo), "Z") + + def test_seek(self): + defn = "char foo; #seek 3; char bar;" + obj = bitwise.parse(defn, b"AbcdZ") + self.assertEqual(str(obj.foo), "A") + self.assertEqual(str(obj.bar), "Z") + + +class TestBitwiseErrors(BaseTest): + def test_missing_semicolon(self): + self.assertRaises(SyntaxError, bitwise.parse, "u8 foo", "") + + +class TestBitwiseComments(BaseTest): + def test_comment_inline_cppstyle(self): + obj = bitwise.parse('u8 foo; // test', b'\x10') + self.assertEqual(16, obj.foo) + + def test_comment_cppstyle(self): + obj = bitwise.parse('// Test this\nu8 foo;', b'\x10') + self.assertEqual(16, obj.foo) + + +class TestBitwiseStringEncoders(BaseTest): + def test_encode_bytes(self): + self.assertEqual(b'foobar\x00', + bitwise.string_straight_encode('foobar\x00')) + + def test_decode_bytes(self): + self.assertEqual('foobar\x00', + bitwise.string_straight_decode(b'foobar\x00')) diff --git a/tests/unit/test_chirp_common.py b/tests/unit/test_chirp_common.py new file mode 100644 index 0000000..400c469 --- /dev/null +++ b/tests/unit/test_chirp_common.py @@ -0,0 +1,415 @@ +import base64 +import json +import os +import tempfile + +import mock + +from tests.unit import base +from chirp import CHIRP_VERSION +from chirp import chirp_common +from chirp import errors + + +class TestUtilityFunctions(base.BaseTest): + def test_parse_freq_whole(self): + self.assertEqual(chirp_common.parse_freq("146.520000"), 146520000) + self.assertEqual(chirp_common.parse_freq("146.5200"), 146520000) + self.assertEqual(chirp_common.parse_freq("146.52"), 146520000) + self.assertEqual(chirp_common.parse_freq("146"), 146000000) + self.assertEqual(chirp_common.parse_freq("1250"), 1250000000) + self.assertEqual(chirp_common.parse_freq("123456789"), + 123456789000000) + + def test_parse_freq_decimal(self): + self.assertEqual(chirp_common.parse_freq("1.0"), 1000000) + self.assertEqual(chirp_common.parse_freq("1.000000"), 1000000) + self.assertEqual(chirp_common.parse_freq("1.1"), 1100000) + self.assertEqual(chirp_common.parse_freq("1.100"), 1100000) + self.assertEqual(chirp_common.parse_freq("0.6"), 600000) + self.assertEqual(chirp_common.parse_freq("0.600"), 600000) + self.assertEqual(chirp_common.parse_freq("0.060"), 60000) + self.assertEqual(chirp_common.parse_freq(".6"), 600000) + + def test_parse_freq_whitespace(self): + self.assertEqual(chirp_common.parse_freq("1 "), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1"), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1 "), 1000000) + + self.assertEqual(chirp_common.parse_freq("1.0 "), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1.0"), 1000000) + self.assertEqual(chirp_common.parse_freq(" 1.0 "), 1000000) + self.assertEqual(chirp_common.parse_freq(""), 0) + self.assertEqual(chirp_common.parse_freq(" "), 0) + + def test_parse_freq_bad(self): + self.assertRaises(ValueError, chirp_common.parse_freq, "a") + self.assertRaises(ValueError, chirp_common.parse_freq, "1.a") + self.assertRaises(ValueError, chirp_common.parse_freq, "a.b") + self.assertRaises(ValueError, chirp_common.parse_freq, + "1.0000001") + + def test_format_freq(self): + self.assertEqual(chirp_common.format_freq(146520000), "146.520000") + self.assertEqual(chirp_common.format_freq(54000000), "54.000000") + self.assertEqual(chirp_common.format_freq(1800000), "1.800000") + self.assertEqual(chirp_common.format_freq(1), "0.000001") + self.assertEqual(chirp_common.format_freq(1250000000), "1250.000000") + + @mock.patch('chirp.CHIRP_VERSION', new='daily-20151021') + def test_compare_version_to_current(self): + self.assertTrue(chirp_common.is_version_newer('daily-20180101')) + self.assertFalse(chirp_common.is_version_newer('daily-20140101')) + self.assertFalse(chirp_common.is_version_newer('0.3.0')) + self.assertFalse(chirp_common.is_version_newer('0.3.0dev')) + + @mock.patch('chirp.CHIRP_VERSION', new='0.3.0dev') + def test_compare_version_to_current_dev(self): + self.assertTrue(chirp_common.is_version_newer('daily-20180101')) + + def test_from_Hz(self): + # FIXME: These are wrong! Adding them here purely to test the + # python3 conversion, but they should be fixed. + self.assertEqual(140, chirp_common.from_GHz(14000000001)) + self.assertEqual(140, chirp_common.from_MHz(14000001)) + self.assertEqual(140, chirp_common.from_kHz(14001)) + + +class TestSplitTone(base.BaseTest): + def _test_split_tone_decode(self, tx, rx, **vals): + mem = chirp_common.Memory() + chirp_common.split_tone_decode(mem, tx, rx) + for key, value in list(vals.items()): + self.assertEqual(getattr(mem, key), value) + + def test_split_tone_decode_none(self): + self._test_split_tone_decode((None, None, None), + (None, None, None), + tmode='') + + def test_split_tone_decode_tone(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('', 0, None), + tmode='Tone', + rtone=100.0) + + def test_split_tone_decode_tsql(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('Tone', 100.0, None), + tmode='TSQL', + ctone=100.0) + + def test_split_tone_decode_dtcs(self): + self._test_split_tone_decode(('DTCS', 23, None), + ('DTCS', 23, None), + tmode='DTCS', + dtcs=23) + + def test_split_tone_decode_cross_tone_tone(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('Tone', 123.0, None), + tmode='Cross', + cross_mode='Tone->Tone', + rtone=100.0, + ctone=123.0) + + def test_split_tone_decode_cross_tone_dtcs(self): + self._test_split_tone_decode(('Tone', 100.0, None), + ('DTCS', 32, 'R'), + tmode='Cross', + cross_mode='Tone->DTCS', + rtone=100.0, + rx_dtcs=32, + dtcs_polarity='NR') + + def test_split_tone_decode_cross_dtcs_tone(self): + self._test_split_tone_decode(('DTCS', 32, 'R'), + ('Tone', 100.0, None), + tmode='Cross', + cross_mode='DTCS->Tone', + ctone=100.0, + dtcs=32, + dtcs_polarity='RN') + + def test_split_tone_decode_cross_dtcs_dtcs(self): + self._test_split_tone_decode(('DTCS', 32, 'R'), + ('DTCS', 25, 'R'), + tmode='Cross', + cross_mode='DTCS->DTCS', + dtcs=32, + rx_dtcs=25, + dtcs_polarity='RR') + + def test_split_tone_decode_cross_none_dtcs(self): + self._test_split_tone_decode((None, None, None), + ('DTCS', 25, 'R'), + tmode='Cross', + cross_mode='->DTCS', + rx_dtcs=25, + dtcs_polarity='NR') + + def test_split_tone_decode_cross_none_tone(self): + self._test_split_tone_decode((None, None, None), + ('Tone', 100.0, None), + tmode='Cross', + cross_mode='->Tone', + ctone=100.0) + + def _set_mem(self, **vals): + mem = chirp_common.Memory() + for key, value in list(vals.items()): + setattr(mem, key, value) + return chirp_common.split_tone_encode(mem) + + def split_tone_encode_test_none(self): + self.assertEqual(self._set_mem(tmode=''), + (('', None, None), + ('', None, None))) + + def split_tone_encode_test_tone(self): + self.assertEqual(self._set_mem(tmode='Tone', rtone=100.0), + (('Tone', 100.0, None), + ('', None, None))) + + def split_tone_encode_test_tsql(self): + self.assertEqual(self._set_mem(tmode='TSQL', ctone=100.0), + (('Tone', 100.0, None), + ('Tone', 100.0, None))) + + def split_tone_encode_test_dtcs(self): + self.assertEqual(self._set_mem(tmode='DTCS', dtcs=23, + dtcs_polarity='RN'), + (('DTCS', 23, 'R'), + ('DTCS', 23, 'N'))) + + def split_tone_encode_test_cross_tone_tone(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='Tone->Tone', + rtone=100.0, ctone=123.0), + (('Tone', 100.0, None), + ('Tone', 123.0, None))) + + def split_tone_encode_test_cross_tone_dtcs(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='Tone->DTCS', + rtone=100.0, rx_dtcs=25), + (('Tone', 100.0, None), + ('DTCS', 25, 'N'))) + + def split_tone_encode_test_cross_dtcs_tone(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='DTCS->Tone', + ctone=100.0, dtcs=25), + (('DTCS', 25, 'N'), + ('Tone', 100.0, None))) + + def split_tone_encode_test_cross_none_dtcs(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='->DTCS', + rx_dtcs=25), + (('', None, None), + ('DTCS', 25, 'N'))) + + def split_tone_encode_test_cross_none_tone(self): + self.assertEqual(self._set_mem(tmode='Cross', cross_mode='->Tone', + ctone=100.0), + (('', None, None), + ('Tone', 100.0, None))) + + +class TestStepFunctions(base.BaseTest): + _625 = [145856250, + 445856250, + 862731250, + 146118750, + ] + _125 = [145862500, + 445862500, + 862737500, + ] + _005 = [145005000, + 445005000, + 850005000, + ] + _025 = [145002500, + 445002500, + 850002500, + ] + + def test_is_fractional_step(self): + for freq in self._125 + self._625: + print(freq) + self.assertTrue(chirp_common.is_fractional_step(freq)) + + def test_is_6_25(self): + for freq in self._625: + self.assertTrue(chirp_common.is_6_25(freq)) + + def test_is_12_5(self): + for freq in self._125: + self.assertTrue(chirp_common.is_12_5(freq)) + + def test_is_5_0(self): + for freq in self._005: + self.assertTrue(chirp_common.is_5_0(freq)) + + def test_is_2_5(self): + for freq in self._025: + self.assertTrue(chirp_common.is_2_5(freq)) + + def test_required_step(self): + steps = {2.5: self._025, + 5.0: self._005, + 6.25: self._625, + 12.5: self._125, + } + for step, freqs in list(steps.items()): + for freq in freqs: + self.assertEqual(step, chirp_common.required_step(freq)) + + def test_required_step_fail(self): + self.assertRaises(errors.InvalidDataError, + chirp_common.required_step, + 146520500) + + def test_fix_rounded_step_250(self): + self.assertEqual(146106250, + chirp_common.fix_rounded_step(146106000)) + + def test_fix_rounded_step_500(self): + self.assertEqual(146112500, + chirp_common.fix_rounded_step(146112000)) + + def test_fix_rounded_step_750(self): + self.assertEqual(146118750, + chirp_common.fix_rounded_step(146118000)) + + +class TestImageMetadata(base.BaseTest): + def test_make_metadata(self): + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + raw_metadata = TestRadio._make_metadata() + metadata = json.loads(base64.b64decode(raw_metadata).decode()) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, metadata) + + def test_strip_metadata(self): + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + raw_metadata = TestRadio._make_metadata() + raw_data = (b'foooooooooooooooooooooo' + TestRadio.MAGIC + + TestRadio._make_metadata()) + data, metadata = chirp_common.FileBackedRadio._strip_metadata(raw_data) + self.assertEqual(b'foooooooooooooooooooooo', data) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, metadata) + + def test_load_mmap_no_metadata(self): + f = tempfile.NamedTemporaryFile() + f.write(b'thisisrawdata') + f.flush() + + with mock.patch('chirp.memmap.MemoryMap') as mock_mmap: + chirp_common.FileBackedRadio(None).load_mmap(f.name) + mock_mmap.assert_called_once_with(b'thisisrawdata') + + def test_load_mmap_bad_metadata(self): + f = tempfile.NamedTemporaryFile() + f.write(b'thisisrawdata') + f.write(chirp_common.FileBackedRadio.MAGIC + b'bad') + f.flush() + + with mock.patch('chirp.memmap.MemoryMap') as mock_mmap: + chirp_common.FileBackedRadio(None).load_mmap(f.name) + mock_mmap.assert_called_once_with(b'thisisrawdata') + + def test_save_mmap_includes_metadata(self): + # Make sure that a file saved with a .img extension includes + # the metadata blob + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + with tempfile.NamedTemporaryFile(suffix='.Img') as f: + fn = f.name + r = TestRadio(None) + r._mmap = mock.Mock() + r._mmap.get_byte_compatible.return_value.get_packed.return_value = ( + b'thisisrawdata') + r.save_mmap(fn) + with open(fn, 'rb') as f: + filedata = f.read() + os.remove(fn) + data, metadata = chirp_common.FileBackedRadio._strip_metadata(filedata) + self.assertEqual(b'thisisrawdata', data) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, metadata) + + def test_save_mmap_no_metadata_not_img_file(self): + # Make sure that if we save without a .img extension we do + # not include the metadata blob + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + with tempfile.NamedTemporaryFile(suffix='.txt') as f: + fn = f.name + r = TestRadio(None) + r._mmap = mock.Mock() + r._mmap.get_byte_compatible.return_value.get_packed.return_value = ( + b'thisisrawdata') + r.save_mmap(fn) + with open(fn, 'rb') as f: + filedata = f.read() + os.remove(fn) + data, metadata = chirp_common.FileBackedRadio._strip_metadata(filedata) + self.assertEqual(b'thisisrawdata', data) + self.assertEqual({}, metadata) + + def test_load_mmap_saves_metadata_on_radio(self): + class TestRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + + with tempfile.NamedTemporaryFile(suffix='.img') as f: + fn = f.name + r = TestRadio(None) + r._mmap = mock.Mock() + r._mmap.get_byte_compatible.return_value.get_packed.return_value = ( + b'thisisrawdata') + r.save_mmap(fn) + + newr = TestRadio(None) + newr.load_mmap(fn) + expected = { + 'vendor': 'Dan', + 'model': 'Foomaster 9000', + 'variant': 'R', + 'rclass': 'TestRadio', + 'chirp_version': CHIRP_VERSION, + } + self.assertEqual(expected, newr.metadata) diff --git a/tests/unit/test_directory.py b/tests/unit/test_directory.py new file mode 100644 index 0000000..d48bb48 --- /dev/null +++ b/tests/unit/test_directory.py @@ -0,0 +1,69 @@ +import base64 +import json +import tempfile + +from tests.unit import base +from chirp import chirp_common +from chirp import directory + + +class TestDirectory(base.BaseTest): + def setUp(self): + super(TestDirectory, self).setUp() + + directory.enable_reregistrations() + + class FakeAlias(chirp_common.Alias): + VENDOR = 'Taylor' + MODEL = 'Barmaster 2000' + VARIANT = 'A' + + @directory.register + class FakeRadio(chirp_common.FileBackedRadio): + VENDOR = 'Dan' + MODEL = 'Foomaster 9000' + VARIANT = 'R' + ALIASES = [FakeAlias] + + @classmethod + def match_model(cls, file_data, image_file): + return file_data == b'thisisrawdata' + + self.test_class = FakeRadio + + def _test_detect_finds_our_class(self, tempfn): + radio = directory.get_radio_by_image(tempfn) + self.assertTrue(isinstance(radio, self.test_class)) + return radio + + def test_detect_with_no_metadata(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b'thisisrawdata') + f.flush() + self._test_detect_finds_our_class(f.name) + + def test_detect_with_metadata_base_class(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b'thisisrawdata') + f.write(self.test_class.MAGIC + b'-') + f.write(self.test_class._make_metadata()) + f.flush() + self._test_detect_finds_our_class(f.name) + + def test_detect_with_metadata_alias_class(self): + with tempfile.NamedTemporaryFile() as f: + f.write(b'thisisrawdata') + f.write(self.test_class.MAGIC + b'-') + FakeAlias = self.test_class.ALIASES[0] + fake_metadata = base64.b64encode(json.dumps( + {'vendor': FakeAlias.VENDOR, + 'model': FakeAlias.MODEL, + 'variant': FakeAlias.VARIANT, + }).encode()) + f.write(fake_metadata) + f.flush() + radio = self._test_detect_finds_our_class(f.name) + self.assertEqual('Taylor', radio.VENDOR) + self.assertEqual('Barmaster 2000', radio.MODEL) + self.assertEqual('A', radio.VARIANT) + diff --git a/tests/unit/test_icom_clone.py b/tests/unit/test_icom_clone.py new file mode 100644 index 0000000..9ade954 --- /dev/null +++ b/tests/unit/test_icom_clone.py @@ -0,0 +1,117 @@ +# Copyright 2019 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from builtins import bytes + +import glob +import os +import logging +import unittest + +from chirp import directory +directory.safe_import_drivers() +from chirp.drivers import icf +from chirp import memmap +from tests import icom_clone_simulator + + +class BaseIcomCloneTest(): + def setUp(self): + self.radio = directory.get_radio(self.RADIO_IDENT)(None) + self.simulator = icom_clone_simulator.FakeIcomRadio(self.radio) + self.radio.set_pipe(self.simulator) + + def image_filename(self, filename): + tests_base = os.path.dirname(os.path.abspath(__file__)) + return os.path.join(tests_base, 'images', filename) + + def load_from_test_image(self, filename): + self.simulator.load_from_file(self.image_filename(filename)) + + def image_version(self, filename): + end = self.radio.get_memsize() + + with open(self.image_filename(filename), 'rb') as f: + data = f.read() + if data[-8:-6] == b'%02x' % self.radio.get_model()[0]: + return 2 + elif data[end-16:end] == b'IcomCloneFormat3': + return 3 + + + def test_sync_in(self): + test_file = self.IMAGE_FILE + self.load_from_test_image(test_file) + self.radio.sync_in() + + img_ver = self.image_version(test_file) + if img_ver == 2: + endstring = b''.join(b'%02x' % ord(c) + for c in self.radio._model[:2]) + self.assertEqual(endstring + b'0001', self.radio._mmap[-8:]) + elif img_ver == 3: + self.assertEqual(b'IcomCloneFormat3', self.radio._mmap[-16:]) + elif img_ver is None: + self.assertEqual(self.radio.get_memsize(), len(self.radio._mmap)) + + def test_sync_out(self): + self.radio._mmap = memmap.MemoryMapBytes( + bytes(b'\x00') * self.radio.get_memsize()) + self.radio._mmap[50] = bytes(b'abcdefgh') + self.radio.sync_out() + self.assertEqual(b'abcdefgh', self.simulator._memory[50:58]) + + +class TestRawRadioData(unittest.TestCase): + def test_get_payload(self): + radio = directory.get_radio('Icom_IC-2730A')(None) + payload = radio.get_payload(bytes(b'\x00\x10\xFE\x00'), True, True) + self.assertEqual(b'\x00\x10\xFF\x0E\x00\xF2', payload) + + payload = radio.get_payload(bytes(b'\x00\x10\xFE\x00'), True, False) + self.assertEqual(b'\x00\x10\xFF\x0E\x00', payload) + + def test_process_frame_payload(self): + radio = directory.get_radio('Icom_IC-2730A')(None) + data = radio.process_frame_payload(bytes(b'\x00\x10\xFF\x0E\x00')) + self.assertEqual(b'\x00\x10\xFE\x00', data) + + +class TestAdapterMeta(type): + def __new__(cls, name, parents, dct): + return super(TestAdapterMeta, cls).__new__(cls, name, parents, dct) + + +test_file_glob = os.path.join(os.path.dirname(os.path.abspath(__file__)), + '..', 'images', + 'Icom_*.img') +import sys +for image_file in glob.glob(test_file_glob): + base, _ext = os.path.splitext(os.path.basename(image_file)) + + try: + radio = directory.get_radio(base) + except Exception: + continue + + if issubclass(radio, icf.IcomRawCloneModeRadio): + # The simulator does not behave like a raw radio + continue + + class_name = 'Test_%s' % base + sys.modules[__name__].__dict__[class_name] = \ + TestAdapterMeta(class_name, + (BaseIcomCloneTest, unittest.TestCase), + dict(RADIO_IDENT=base, IMAGE_FILE=image_file)) diff --git a/tests/unit/test_import_logic.py b/tests/unit/test_import_logic.py new file mode 100644 index 0000000..ecd9aa9 --- /dev/null +++ b/tests/unit/test_import_logic.py @@ -0,0 +1,373 @@ +from tests.unit import base +from chirp import import_logic +from chirp import chirp_common +from chirp import errors + + +class FakeRadio(chirp_common.Radio): + def __init__(self, arg): + self.POWER_LEVELS = list([chirp_common.PowerLevel('lo', watts=5), + chirp_common.PowerLevel('hi', watts=50)]) + self.TMODES = list(['', 'Tone', 'TSQL']) + self.HAS_CTONE = True + self.HAS_RX_DTCS = False + self.MODES = list(['FM', 'AM', 'DV']) + self.DUPLEXES = list(['', '-', '+']) + + def filter_name(self, name): + return 'filtered-name' + + def get_features(self): + rf = chirp_common.RadioFeatures() + rf.valid_power_levels = self.POWER_LEVELS + rf.valid_tmodes = self.TMODES + rf.valid_modes = self.MODES + rf.valid_duplexes = self.DUPLEXES + rf.has_ctone = self.HAS_CTONE + rf.has_rx_dtcs = self.HAS_RX_DTCS + return rf + + +class FakeDstarRadio(FakeRadio, chirp_common.IcomDstarSupport): + pass + + +class DstarTests(base.BaseTest): + def _test_ensure_has_calls(self, mem, + ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls): + radio = FakeDstarRadio(None) + self.mox.StubOutWithMock(radio, 'get_urcall_list') + self.mox.StubOutWithMock(radio, 'get_repeater_call_list') + radio.get_urcall_list().AndReturn(ini_urcalls) + radio.get_repeater_call_list().AndReturn(ini_rptcalls) + self.mox.ReplayAll() + import_logic.ensure_has_calls(radio, mem) + self.assertEqual(sorted(ini_urcalls), sorted(exp_urcalls)) + self.assertEqual(sorted(ini_rptcalls), sorted(exp_rptcalls)) + + def test_ensure_has_calls_empty(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['', '', '', '', ''] + ini_rptcalls = ['', '', '', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[0] = mem.dv_urcall + exp_rptcalls[0] = mem.dv_rpt1call + exp_rptcalls[1] = mem.dv_rpt2call + self._test_ensure_has_calls(mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_partial(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', '', '', ''] + ini_rptcalls = ['FOO', 'BAR', '', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[2] = mem.dv_urcall + exp_rptcalls[2] = mem.dv_rpt1call + exp_rptcalls[3] = mem.dv_rpt2call + self._test_ensure_has_calls(mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_almost_full(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self._test_ensure_has_calls(mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_urcall_full(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', 'BOOM'] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', '', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self.assertRaises(errors.RadioError, + self._test_ensure_has_calls, + mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_rptcall_full1(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self.assertRaises(errors.RadioError, + self._test_ensure_has_calls, + mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + def test_ensure_has_calls_rptcall_full2(self): + mem = chirp_common.DVMemory() + mem.dv_urcall = 'KK7DS' + mem.dv_rpt1call = 'KD7RFI B' + mem.dv_rpt2call = 'KD7RFI G' + ini_urcalls = ['FOO', 'BAR', 'BAZ', 'BAT', ''] + ini_rptcalls = ['FOO', 'BAR', 'BAZ', 'BAT', 'BOOM'] + exp_urcalls = list(ini_urcalls) + exp_rptcalls = list(ini_rptcalls) + exp_urcalls[4] = mem.dv_urcall + exp_rptcalls[3] = mem.dv_rpt1call + exp_rptcalls[4] = mem.dv_rpt2call + self.assertRaises(errors.RadioError, + self._test_ensure_has_calls, + mem, ini_urcalls, ini_rptcalls, + exp_urcalls, exp_rptcalls) + + +class ImportFieldTests(base.BaseTest): + def test_import_name(self): + mem = chirp_common.Memory() + mem.name = 'foo' + import_logic._import_name(FakeRadio(None), None, mem) + self.assertEqual(mem.name, 'filtered-name') + + def test_import_power_same(self): + radio = FakeRadio(None) + same_rf = radio.get_features() + mem = chirp_common.Memory() + mem.power = same_rf.valid_power_levels[0] + import_logic._import_power(radio, same_rf, mem) + self.assertEqual(mem.power, same_rf.valid_power_levels[0]) + + def test_import_power_no_src(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + mem = chirp_common.Memory() + mem.power = None + import_logic._import_power(radio, src_rf, mem) + self.assertEqual(mem.power, radio.POWER_LEVELS[0]) + + def test_import_power_no_dst(self): + radio = FakeRadio(None) + src_rf = radio.get_features() # Steal a copy before we stub out + self.mox.StubOutWithMock(radio, 'get_features') + radio.get_features().AndReturn(chirp_common.RadioFeatures()) + self.mox.ReplayAll() + mem = chirp_common.Memory() + mem.power = src_rf.valid_power_levels[0] + import_logic._import_power(radio, src_rf, mem) + self.assertEqual(mem.power, None) + + def test_import_power_closest(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + src_rf.valid_power_levels = [ + chirp_common.PowerLevel('foo', watts=7), + chirp_common.PowerLevel('bar', watts=51), + chirp_common.PowerLevel('baz', watts=1), + ] + mem = chirp_common.Memory() + mem.power = src_rf.valid_power_levels[0] + import_logic._import_power(radio, src_rf, mem) + self.assertEqual(mem.power, radio.POWER_LEVELS[0]) + + def test_import_tone_diffA_tsql(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + src_rf.has_ctone = False + mem = chirp_common.Memory() + mem.tmode = 'TSQL' + mem.rtone = 100.0 + import_logic._import_tone(radio, src_rf, mem) + self.assertEqual(mem.ctone, 100.0) + + def test_import_tone_diffB_tsql(self): + radio = FakeRadio(None) + radio.HAS_CTONE = False + src_rf = chirp_common.RadioFeatures() + src_rf.has_ctone = True + mem = chirp_common.Memory() + mem.tmode = 'TSQL' + mem.ctone = 100.0 + import_logic._import_tone(radio, src_rf, mem) + self.assertEqual(mem.rtone, 100.0) + + def test_import_dtcs_diffA_dtcs(self): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + src_rf.has_rx_dtcs = True + mem = chirp_common.Memory() + mem.tmode = 'DTCS' + mem.rx_dtcs = 32 + import_logic._import_dtcs(radio, src_rf, mem) + self.assertEqual(mem.dtcs, 32) + + def test_import_dtcs_diffB_dtcs(self): + radio = FakeRadio(None) + radio.HAS_RX_DTCS = True + src_rf = chirp_common.RadioFeatures() + src_rf.has_rx_dtcs = False + mem = chirp_common.Memory() + mem.tmode = 'DTCS' + mem.dtcs = 32 + import_logic._import_dtcs(radio, src_rf, mem) + self.assertEqual(mem.rx_dtcs, 32) + + def test_import_mode_valid_fm(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.mode = 'Auto' + mem.freq = 146000000 + import_logic._import_mode(radio, None, mem) + self.assertEqual(mem.mode, 'FM') + + def test_import_mode_valid_am(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.mode = 'Auto' + mem.freq = 18000000 + import_logic._import_mode(radio, None, mem) + self.assertEqual(mem.mode, 'AM') + + def test_import_mode_invalid(self): + radio = FakeRadio(None) + radio.MODES.remove('AM') + mem = chirp_common.Memory() + mem.mode = 'Auto' + mem.freq = 1800000 + self.assertRaises(import_logic.DestNotCompatible, + import_logic._import_mode, radio, None, mem) + + def test_import_duplex_vhf(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 146000000 + mem.offset = 146600000 + mem.duplex = 'split' + import_logic._import_duplex(radio, None, mem) + self.assertEqual(mem.duplex, '+') + self.assertEqual(mem.offset, 600000) + + def test_import_duplex_negative(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 146600000 + mem.offset = 146000000 + mem.duplex = 'split' + import_logic._import_duplex(radio, None, mem) + self.assertEqual(mem.duplex, '-') + self.assertEqual(mem.offset, 600000) + + def test_import_duplex_uhf(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 431000000 + mem.offset = 441000000 + mem.duplex = 'split' + import_logic._import_duplex(radio, None, mem) + self.assertEqual(mem.duplex, '+') + self.assertEqual(mem.offset, 10000000) + + def test_import_duplex_too_big_vhf(self): + radio = FakeRadio(None) + mem = chirp_common.Memory() + mem.freq = 146000000 + mem.offset = 246000000 + mem.duplex = 'split' + self.assertRaises(import_logic.DestNotCompatible, + import_logic._import_duplex, radio, None, mem) + + def test_import_mem(self, errors=[]): + radio = FakeRadio(None) + src_rf = chirp_common.RadioFeatures() + mem = chirp_common.Memory() + + self.mox.StubOutWithMock(mem, 'dupe') + self.mox.StubOutWithMock(import_logic, '_import_name') + self.mox.StubOutWithMock(import_logic, '_import_power') + self.mox.StubOutWithMock(import_logic, '_import_tone') + self.mox.StubOutWithMock(import_logic, '_import_dtcs') + self.mox.StubOutWithMock(import_logic, '_import_mode') + self.mox.StubOutWithMock(import_logic, '_import_duplex') + self.mox.StubOutWithMock(radio, 'validate_memory') + + mem.dupe().AndReturn(mem) + import_logic._import_name(radio, src_rf, mem) + import_logic._import_power(radio, src_rf, mem) + import_logic._import_tone(radio, src_rf, mem) + import_logic._import_dtcs(radio, src_rf, mem) + import_logic._import_mode(radio, src_rf, mem) + import_logic._import_duplex(radio, src_rf, mem) + radio.validate_memory(mem).AndReturn(errors) + + self.mox.ReplayAll() + + import_logic.import_mem(radio, src_rf, mem) + + def test_import_mem_with_warnings(self): + self.test_import_mem([chirp_common.ValidationWarning('Test')]) + + def test_import_mem_with_errors(self): + self.assertRaises(import_logic.DestNotCompatible, + self.test_import_mem, + [chirp_common.ValidationError('Test')]) + + def test_import_bank(self): + dst_mem = chirp_common.Memory() + dst_mem.number = 1 + src_mem = chirp_common.Memory() + src_mem.number = 2 + dst_radio = FakeRadio(None) + src_radio = FakeRadio(None) + dst_bm = chirp_common.BankModel(dst_radio) + src_bm = chirp_common.BankModel(src_radio) + + dst_banks = [chirp_common.Bank(dst_bm, 0, 'A'), + chirp_common.Bank(dst_bm, 1, 'B'), + chirp_common.Bank(dst_bm, 2, 'C'), + ] + src_banks = [chirp_common.Bank(src_bm, 1, '1'), + chirp_common.Bank(src_bm, 2, '2'), + chirp_common.Bank(src_bm, 3, '3'), + ] + + self.mox.StubOutWithMock(dst_radio, 'get_mapping_models') + self.mox.StubOutWithMock(src_radio, 'get_mapping_models') + self.mox.StubOutWithMock(dst_bm, 'get_mappings') + self.mox.StubOutWithMock(src_bm, 'get_mappings') + self.mox.StubOutWithMock(dst_bm, 'get_memory_mappings') + self.mox.StubOutWithMock(src_bm, 'get_memory_mappings') + self.mox.StubOutWithMock(dst_bm, 'remove_memory_from_mapping') + self.mox.StubOutWithMock(dst_bm, 'add_memory_to_mapping') + + dst_radio.get_mapping_models().AndReturn([dst_bm]) + dst_bm.get_mappings().AndReturn(dst_banks) + src_radio.get_mapping_models().AndReturn([src_bm]) + src_bm.get_mappings().AndReturn(src_banks) + src_bm.get_memory_mappings(src_mem).AndReturn([src_banks[0]]) + dst_bm.get_memory_mappings(dst_mem).AndReturn([dst_banks[1]]) + dst_bm.remove_memory_from_mapping(dst_mem, dst_banks[1]) + dst_bm.add_memory_to_mapping(dst_mem, dst_banks[0]) + + self.mox.ReplayAll() + + import_logic.import_bank(dst_radio, src_radio, dst_mem, src_mem) diff --git a/tests/unit/test_mappingmodel.py b/tests/unit/test_mappingmodel.py new file mode 100644 index 0000000..2f263d7 --- /dev/null +++ b/tests/unit/test_mappingmodel.py @@ -0,0 +1,283 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.unit import base +from chirp import chirp_common +from chirp.drivers import icf + + +class TestBaseMapping(base.BaseTest): + CLS = chirp_common.MemoryMapping + + def test_mapping(self): + model = chirp_common.MappingModel(None, 'Foo') + mapping = self.CLS(model, 1, 'Foo') + self.assertEqual(str(mapping), 'Foo') + self.assertEqual(mapping.get_name(), 'Foo') + self.assertEqual(mapping.get_index(), 1) + self.assertEqual(repr(mapping), '%s-1' % self.CLS.__name__) + self.assertEqual(mapping._model, model) + + def test_mapping_eq(self): + mapping1 = self.CLS(None, 1, 'Foo') + mapping2 = self.CLS(None, 1, 'Bar') + mapping3 = self.CLS(None, 2, 'Foo') + + self.assertEqual(mapping1, mapping2) + self.assertNotEqual(mapping1, mapping3) + + +class TestBaseBank(TestBaseMapping): + CLS = chirp_common.Bank + + +class _TestBaseClass(base.BaseTest): + ARGS = tuple() + + def setUp(self): + super(_TestBaseClass, self).setUp() + self.model = self.CLS(*self.ARGS) + + def _test_base(self, method, *args): + self.assertRaises(NotImplementedError, + getattr(self.model, method), *args) + + +class TestBaseMappingModel(_TestBaseClass): + CLS = chirp_common.MappingModel + ARGS = tuple([None, 'Foo']) + + def test_base_class(self): + methods = [('get_num_mappings', ()), + ('get_mappings', ()), + ('add_memory_to_mapping', (None, None)), + ('remove_memory_from_mapping', (None, None)), + ('get_mapping_memories', (None,)), + ('get_memory_mappings', (None,)), + ] + for method, args in methods: + self._test_base(method, *args) + + def test_get_name(self): + self.assertEqual(self.model.get_name(), 'Foo') + + +class TestBaseBankModel(TestBaseMappingModel): + ARGS = tuple([None]) + CLS = chirp_common.BankModel + + def test_get_name(self): + self.assertEqual(self.model.get_name(), 'Banks') + + +class TestBaseMappingModelIndexInterface(_TestBaseClass): + CLS = chirp_common.MappingModelIndexInterface + + def test_base_class(self): + methods = [('get_index_bounds', ()), + ('get_memory_index', (None, None)), + ('set_memory_index', (None, None, None)), + ('get_next_mapping_index', (None,)), + ] + for method, args in methods: + self._test_base(method, *args) + + +class TestIcomBanks(TestBaseMapping): + def test_icom_bank(self): + bank = icf.IcomBank(None, 1, 'Foo') + # IcomBank has an index attribute used by IcomBankModel + self.assertTrue(hasattr(bank, 'index')) + + +class TestIcomBankModel(base.BaseTest): + CLS = icf.IcomBankModel + + def _get_rf(self): + rf = chirp_common.RadioFeatures() + rf.memory_bounds = (1, 10) + return rf + + def setUp(self): + super(TestIcomBankModel, self).setUp() + + class FakeRadio(icf.IcomCloneModeRadio): + _num_banks = 10 + _bank_index_bounds = (0, 10) + + def get_features(the_radio): + return self._get_rf() + + def _set_bank(self, number, index): + pass + + def _get_bank(self, number): + pass + + def _get_bank_index(self, number): + pass + + def _set_bank_index(self, number, index): + pass + + def get_memory(self, number): + pass + + self._radio = FakeRadio(None) + self._model = self.CLS(self._radio) + self.mox.StubOutWithMock(self._radio, '_set_bank') + self.mox.StubOutWithMock(self._radio, '_get_bank') + self.mox.StubOutWithMock(self._radio, '_set_bank_index') + self.mox.StubOutWithMock(self._radio, '_get_bank_index') + self.mox.StubOutWithMock(self._radio, 'get_memory') + + def test_get_num_mappings(self): + self.assertEqual(self._model.get_num_mappings(), 10) + + def test_get_mappings(self): + banks = self._model.get_mappings() + self.assertEqual(len(banks), 10) + i = 0 + for bank in banks: + index = chr(ord("A") + i) + self.assertEqual(bank.get_index(), index) + self.assertEqual(bank.get_name(), 'BANK-%s' % index) + self.assertEqual(bank.index, i) + i += 1 + + def test_add_memory_to_mapping(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + bank = banks[2] + self._radio._set_bank(5, 2) + self.mox.ReplayAll() + self._model.add_memory_to_mapping(mem, bank) + + def _setup_test_remove_memory_from_mapping(self, curbank): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + bank = banks[2] + self._radio._get_bank(5).AndReturn(curbank) + if curbank == 2: + self._radio._set_bank(5, None) + self.mox.ReplayAll() + return mem, bank + + def test_remove_memory_from_mapping(self): + mem, bank = self._setup_test_remove_memory_from_mapping(2) + self._model.remove_memory_from_mapping(mem, bank) + + def test_remove_memory_from_mapping_wrong_bank(self): + mem, bank = self._setup_test_remove_memory_from_mapping(3) + self.assertRaises(Exception, + self._model.remove_memory_from_mapping, mem, bank) + + def test_remove_memory_from_mapping_no_bank(self): + mem, bank = self._setup_test_remove_memory_from_mapping(None) + self.assertRaises(Exception, + self._model.remove_memory_from_mapping, mem, bank) + + def test_get_mapping_memories(self): + banks = self._model.get_mappings() + expected = [] + for i in range(1, 10): + should_include = bool(i % 2) + self._radio._get_bank(i).AndReturn( + should_include and banks[1].index or None) + if should_include: + self._radio.get_memory(i).AndReturn(i) + expected.append(i) + self.mox.ReplayAll() + members = self._model.get_mapping_memories(banks[1]) + self.assertEqual(members, expected) + + def test_get_memory_mappings(self): + banks = self._model.get_mappings() + mem1 = chirp_common.Memory() + mem1.number = 5 + mem2 = chirp_common.Memory() + mem2.number = 6 + self._radio._get_bank(mem1.number).AndReturn(2) + self._radio._get_bank(mem2.number).AndReturn(None) + self.mox.ReplayAll() + self.assertEqual(self._model.get_memory_mappings(mem1)[0], banks[2]) + self.assertEqual(self._model.get_memory_mappings(mem2), []) + + +class TestIcomIndexedBankModel(TestIcomBankModel): + CLS = icf.IcomIndexedBankModel + + def _get_rf(self): + rf = super(TestIcomIndexedBankModel, self)._get_rf() + rf.has_bank_index = True + return rf + + def test_get_index_bounds(self): + self.assertEqual(self._model.get_index_bounds(), (0, 10)) + + def test_get_memory_index(self): + mem = chirp_common.Memory() + mem.number = 5 + self._radio._get_bank_index(mem.number).AndReturn(1) + self.mox.ReplayAll() + self.assertEqual(self._model.get_memory_index(mem, None), 1) + + def test_set_memory_index(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + self.mox.StubOutWithMock(self._model, 'get_memory_mappings') + self._model.get_memory_mappings(mem).AndReturn([banks[3]]) + self._radio._set_bank_index(mem.number, 1) + self.mox.ReplayAll() + self._model.set_memory_index(mem, banks[3], 1) + + def test_set_memory_index_bad_bank(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + self.mox.StubOutWithMock(self._model, 'get_memory_mappings') + self._model.get_memory_mappings(mem).AndReturn([banks[4]]) + self.mox.ReplayAll() + self.assertRaises(Exception, + self._model.set_memory_index, mem, banks[3], 1) + + def test_set_memory_index_bad_index(self): + mem = chirp_common.Memory() + mem.number = 5 + banks = self._model.get_mappings() + self.mox.StubOutWithMock(self._model, 'get_memory_mappings') + self._model.get_memory_mappings(mem).AndReturn([banks[3]]) + self.mox.ReplayAll() + self.assertRaises(Exception, + self._model.set_memory_index, mem, banks[3], 99) + + def test_get_next_mapping_index(self): + banks = self._model.get_mappings() + for i in range(*self._radio.get_features().memory_bounds): + self._radio._get_bank(i).AndReturn((i % 2) and banks[1].index) + if bool(i % 2): + self._radio._get_bank_index(i).AndReturn(i) + idx = 0 + for i in range(*self._radio.get_features().memory_bounds): + self._radio._get_bank(i).AndReturn((i % 2) and banks[2].index) + if i % 2: + self._radio._get_bank_index(i).AndReturn(idx) + idx += 1 + self.mox.ReplayAll() + self.assertEqual(self._model.get_next_mapping_index(banks[1]), 0) + self.assertEqual(self._model.get_next_mapping_index(banks[2]), 5) diff --git a/tests/unit/test_memedit_edits.py b/tests/unit/test_memedit_edits.py new file mode 100644 index 0000000..fdf971c --- /dev/null +++ b/tests/unit/test_memedit_edits.py @@ -0,0 +1,82 @@ +try: + import mox +except ImportError: + from mox3 import mox +from tests.unit import base + +__builtins__["_"] = lambda s: s + +memedit = None + + +class TestEdits(base.BaseTest): + def setUp(self): + global memedit + super(TestEdits, self).setUp() + base.mock_gtk() + from chirp.ui import memedit as memedit_module + memedit = memedit_module + + def tearDown(self): + super(TestEdits, self).tearDown() + base.unmock_gtk() + + def _test_tone_column_change(self, col, + ini_tmode='', ini_cmode='', + exp_tmode=None, exp_cmode=None): + editor = self.mox.CreateMock(memedit.MemoryEditor) + editor._config = self.mox.CreateMockAnything() + editor._config.get_bool("no_smart_tmode").AndReturn(False) + editor.col = lambda x: x + editor.store = self.mox.CreateMockAnything() + editor.store.get_iter('path').AndReturn('iter') + editor.store.get('iter', 'Tone Mode', 'Cross Mode').AndReturn( + (ini_tmode, ini_cmode)) + if exp_tmode: + editor.store.set('iter', 'Tone Mode', exp_tmode) + if exp_cmode and col != 'Cross Mode': + editor.store.set('iter', 'Cross Mode', exp_cmode) + self.mox.ReplayAll() + memedit.MemoryEditor.ed_tone_field(editor, None, 'path', None, col) + + def _test_auto_tone_mode(self, col, exp_tmode, exp_cmode): + cross_exp_cmode = (exp_tmode == "Cross" and exp_cmode or None) + + # No tmode -> expected tmode, maybe requires cross mode change + self._test_tone_column_change(col, exp_tmode=exp_tmode, + exp_cmode=cross_exp_cmode) + + # Expected tmode does not re-set tmode, may change cmode + self._test_tone_column_change(col, ini_tmode=exp_tmode, + exp_cmode=cross_exp_cmode) + + # Invalid tmode -> expected, may change cmode + self._test_tone_column_change(col, ini_tmode="foo", + exp_tmode=exp_tmode, + exp_cmode=cross_exp_cmode) + + # Expected cmode does not re-set cmode + self._test_tone_column_change(col, ini_tmode="Cross", + ini_cmode=exp_cmode) + + # Invalid cmode -> expected + self._test_tone_column_change(col, ini_tmode="Cross", + ini_cmode="foo", exp_cmode=exp_cmode) + + def test_auto_tone_mode_tone(self): + self._test_auto_tone_mode('Tone', 'Tone', 'Tone->Tone') + + def test_auto_tone_mode_tsql(self): + self._test_auto_tone_mode('ToneSql', 'TSQL', 'Tone->Tone') + + def test_auto_tone_mode_dtcs(self): + self._test_auto_tone_mode('DTCS Code', 'DTCS', 'DTCS->') + + def test_auto_tone_mode_dtcs_rx(self): + self._test_auto_tone_mode('DTCS Rx Code', 'Cross', '->DTCS') + + def test_auto_tone_mode_dtcs_pol(self): + self._test_auto_tone_mode('DTCS Pol', 'DTCS', 'DTCS->') + + def test_auto_tone_mode_cross(self): + self._test_auto_tone_mode('Cross Mode', 'Cross', 'Tone->Tone') diff --git a/tests/unit/test_platform.py b/tests/unit/test_platform.py new file mode 100644 index 0000000..394f894 --- /dev/null +++ b/tests/unit/test_platform.py @@ -0,0 +1,62 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import unittest +try: + import mox +except ImportError: + from mox3 import mox +import os + +from tests.unit import base +from chirp import platform + + +class Win32PlatformTest(base.BaseTest): + def _test_init(self): + self.mox.StubOutWithMock(platform, 'comports') + self.mox.StubOutWithMock(os, 'mkdir') + self.mox.StubOutWithMock(os, 'getenv') + os.mkdir(mox.IgnoreArg()) + os.getenv("APPDATA").AndReturn("foo") + os.getenv("USERPROFILE").AndReturn("foo") + + def test_init(self): + self._test_init() + self.mox.ReplayAll() + platform.Win32Platform() + + def test_serial_ports_sorted(self): + self._test_init() + + fake_comports = [] + numbers = [1, 11, 2, 12, 7, 3, 123] + for i in numbers: + fake_comports.append(("COM%i" % i, None, None)) + + platform.comports().AndReturn(fake_comports) + self.mox.ReplayAll() + ports = platform.Win32Platform().list_serial_ports() + + correct_order = ["COM%i" % i for i in sorted(numbers)] + self.assertEqual(ports, correct_order) + + def test_serial_ports_bad_portnames(self): + self._test_init() + + platform.comports().AndReturn([('foo', None, None)]) + self.mox.ReplayAll() + ports = platform.Win32Platform().list_serial_ports() + self.assertEqual(ports, ['foo']) diff --git a/tests/unit/test_repeaterbook.py b/tests/unit/test_repeaterbook.py new file mode 100644 index 0000000..2563b3f --- /dev/null +++ b/tests/unit/test_repeaterbook.py @@ -0,0 +1,28 @@ +import tempfile +import unittest + +from chirp import chirp_common +from chirp.drivers import repeaterbook + + +class TestRepeaterBook(unittest.TestCase): + def _fetch_and_load(self, query): + fn = tempfile.mktemp('.csv') + chirp_common.urlretrieve(query, fn) + radio = repeaterbook.RBRadio(fn) + return fn, radio + + def test_political(self): + query = "http://www.repeaterbook.com/repeaters/downloads/chirp.php" + \ + "?func=default&state_id=%s&band=%s&freq=%%&band6=%%&loc=%%" + \ + "&county_id=%s&status_id=%%&features=%%&coverage=%%&use=%%" + query = query % ('41', '%%', '005') + self._fetch_and_load(query) + + def test_proximity(self): + loc = '97124' + band = '%%' + dist = '20' + query = "https://www.repeaterbook.com/repeaters/downloads/CHIRP/" \ + "app_direct.php?loc=%s&band=%s&dist=%s" % (loc, band, dist) + self._fetch_and_load(query) diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py new file mode 100644 index 0000000..ccc4308 --- /dev/null +++ b/tests/unit/test_settings.py @@ -0,0 +1,140 @@ +# Copyright 2013 Dan Smith +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from tests.unit import base +from chirp import settings + + +class TestSettingValues(base.BaseTest): + def _set_and_test(self, rsv, *values): + for value in values: + rsv.set_value(value) + self.assertEqual(rsv.get_value(), value) + + def _set_and_catch(self, rsv, *values): + for value in values: + self.assertRaises(settings.InvalidValueError, + rsv.set_value, value) + + def test_radio_setting_value_integer(self): + value = settings.RadioSettingValueInteger(0, 10, 5) + self.assertEqual(value.get_value(), 5) + self._set_and_test(value, 1, 0, 10) + self._set_and_catch(value, -1, 11) + + def test_radio_setting_value_float(self): + value = settings.RadioSettingValueFloat(1.0, 10.5, 5.0) + self.assertEqual(value.get_value(), 5.0) + self._set_and_test(value, 2.5, 1.0, 10.5) + self._set_and_catch(value, 0.9, 10.6, -1.5) + + def test_radio_setting_value_boolean(self): + value = settings.RadioSettingValueBoolean(True) + self.assertTrue(value.get_value()) + self._set_and_test(value, True, False) + + def test_radio_setting_value_list(self): + opts = ["Abc", "Def", "Ghi"] + value = settings.RadioSettingValueList(opts, "Abc") + self.assertEqual(value.get_value(), "Abc") + self.assertEqual(int(value), 0) + self._set_and_test(value, "Def", "Ghi", "Abc") + self._set_and_catch(value, "Jkl", "Xyz") + self.assertEqual(value.get_options(), opts) + + def test_radio_setting_value_string(self): + value = settings.RadioSettingValueString(1, 5, "foo", autopad=False) + self.assertEqual(value.get_value(), "foo") + self.assertEqual(str(value), "foo") + self._set_and_test(value, "a", "abc", "abdef") + self._set_and_catch(value, "", "abcdefg") + + def test_validate_callback(self): + class TestException(Exception): + pass + + value = settings.RadioSettingValueString(0, 5, "foo", autopad=False) + + def test_validate(val): + if val == "bar": + raise TestException() + value.set_validate_callback(test_validate) + value.set_value("baz") + self.assertRaises(TestException, value.set_value, "bar") + + def test_changed(self): + value = settings.RadioSettingValueBoolean(False) + self.assertFalse(value.changed()) + value.set_value(False) + self.assertFalse(value.changed()) + value.set_value(True) + self.assertTrue(value.changed()) + + +class TestSettingContainers(base.BaseTest): + def test_radio_setting_group(self): + s1 = settings.RadioSetting("s1", "Setting 1") + s2 = settings.RadioSetting("s2", "Setting 2") + s3 = settings.RadioSetting("s3", "Setting 3") + group = settings.RadioSettingGroup("foo", "Foo Group", s1) + self.assertEqual(group.get_name(), "foo") + self.assertEqual(group.get_shortname(), "Foo Group") + self.assertEqual(group.values(), [s1]) + self.assertEqual(group.keys(), ["s1"]) + group.append(s2) + self.assertEqual(group.items(), [("s1", s1), ("s2", s2)]) + self.assertEqual(group["s1"], s1) + group["s3"] = s3 + self.assertEqual(group.values(), [s1, s2, s3]) + self.assertEqual(group.keys(), ["s1", "s2", "s3"]) + self.assertEqual([x for x in group], [s1, s2, s3]) + + def set_dupe(): + group["s3"] = s3 + self.assertRaises(KeyError, set_dupe) + + def test_radio_setting(self): + val = settings.RadioSettingValueBoolean(True) + rs = settings.RadioSetting("foo", "Foo", val) + self.assertEqual(rs.value, val) + rs.value = False + self.assertEqual(val.get_value(), False) + + def test_radio_setting_multi(self): + val1 = settings.RadioSettingValueBoolean(True) + val2 = settings.RadioSettingValueBoolean(False) + rs = settings.RadioSetting("foo", "Foo", val1, val2) + self.assertEqual(rs[0], val1) + self.assertEqual(rs[1], val2) + rs[0] = False + rs[1] = True + self.assertEqual(val1.get_value(), False) + self.assertEqual(val2.get_value(), True) + + def test_apply_callback(self): + class TestException(Exception): + pass + + rs = settings.RadioSetting("foo", "Foo") + self.assertFalse(rs.has_apply_callback()) + + def test_cb(setting, data1, data2): + self.assertEqual(setting, rs) + self.assertEqual(data1, "foo") + self.assertEqual(data2, "bar") + raise TestException() + rs.set_apply_callback(test_cb, "foo", "bar") + self.assertTrue(rs.has_apply_callback()) + self.assertRaises(TestException, rs.run_apply_callback) diff --git a/tests/unit/test_shiftdialog.py b/tests/unit/test_shiftdialog.py new file mode 100644 index 0000000..fe0b62d --- /dev/null +++ b/tests/unit/test_shiftdialog.py @@ -0,0 +1,112 @@ +import unittest +try: + import mox +except ImportError: + from mox3 import mox + +from tests.unit import base +from chirp import chirp_common +from chirp import errors + +shiftdialog = None + + +class FakeRadio(object): + def __init__(self, *memories): + self._mems = {} + for location in memories: + mem = chirp_common.Memory() + mem.number = location + self._mems[location] = mem + self._features = chirp_common.RadioFeatures() + + def get_features(self): + return self._features + + def get_memory(self, location): + try: + return self._mems[location] + except KeyError: + mem = chirp_common.Memory() + mem.number = location + mem.empty = True + return mem + + def set_memory(self, memory): + self._mems[memory.number] = memory + + def erase_memory(self, location): + del self._mems[location] + + +class FakeRadioThread(object): + def __init__(self, radio): + self.radio = radio + + def lock(self): + pass + + def unlock(self): + pass + + +class ShiftDialogTest(base.BaseTest): + def setUp(self): + global shiftdialog + super(ShiftDialogTest, self).setUp() + base.mock_gtk() + from chirp.ui import shiftdialog as shiftdialog_module + shiftdialog = shiftdialog_module + + def tearDown(self): + super(ShiftDialogTest, self).tearDown() + base.unmock_gtk() + + def _test_hole(self, fn, starting, arg, expected): + radio = FakeRadio(*tuple(starting)) + radio.get_features().memory_bounds = (0, 5) + sd = shiftdialog.ShiftDialog(FakeRadioThread(radio)) + + if isinstance(arg, tuple): + getattr(sd, fn)(*arg) + else: + getattr(sd, fn)(arg) + + self.assertEqual(expected, sorted(radio._mems.keys())) + self.assertEqual(expected, + sorted([mem.number for mem in radio._mems.values()])) + + def _test_delete_hole(self, starting, arg, expected): + self._test_hole('_delete_hole', starting, arg, expected) + + def _test_insert_hole(self, starting, pos, expected): + self._test_hole('_insert_hole', starting, pos, expected) + + def test_delete_hole_with_hole(self): + self._test_delete_hole([1, 2, 3, 5], + 2, + [1, 2, 5]) + + def test_delete_hole_without_hole(self): + self._test_delete_hole([1, 2, 3, 4, 5], + 2, + [1, 2, 3, 4]) + + def test_delete_hole_with_all(self): + self._test_delete_hole([1, 2, 3, 5], + (2, True), + [1, 2, 4]) + + def test_delete_hole_with_all_full(self): + self._test_delete_hole([1, 2, 3, 4, 5], + (2, True), + [1, 2, 3, 4]) + + def test_insert_hole_with_space(self): + self._test_insert_hole([1, 2, 3, 5], + 2, + [1, 3, 4, 5]) + + def test_insert_hole_without_space(self): + self.assertRaises(errors.InvalidMemoryLocation, + self._test_insert_hole, [1, 2, 3, 4, 5], 2, []) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000..575f3e3 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,21 @@ +from chirp import util +from tests.unit import base + + +class TestUtils(base.BaseTest): + def test_hexprint_with_string(self): + util.hexprint('00000000000000') + + def test_hexprint_with_bytes(self): + util.hexprint(b'00000000000000') + + def test_struct_pack(self): + struct = util.StringStruct + + self.assertEqual('\x00c', + struct.pack('bc', 0, 'c')) + + def test_struct_unpack(self): + struct = util.StringStruct + + self.assertEqual((1, 'c'), struct.unpack('bc', '\x01c')) diff --git a/tests/unit/test_yaesu_clone.py b/tests/unit/test_yaesu_clone.py new file mode 100644 index 0000000..869c3b5 --- /dev/null +++ b/tests/unit/test_yaesu_clone.py @@ -0,0 +1,41 @@ +from builtins import bytes +import unittest + +from chirp.drivers import yaesu_clone +from chirp import memmap + + +class TestYaesuChecksum(unittest.TestCase): + def _test_checksum(self, mmap): + cs = yaesu_clone.YaesuChecksum(0, 2, 3) + + self.assertEqual(42, cs.get_existing(mmap)) + self.assertEqual(0x8A, cs.get_calculated(mmap)) + try: + mmap = mmap.get_byte_compatible() + mmap[0] = 3 + except AttributeError: + # str or bytes + try: + # str + mmap = memmap.MemoryMap('\x03' + mmap[1:]) + except TypeError: + # bytes + mmap = memmap.MemoryMapBytes(b'\x03' + mmap[1:]) + + cs.update(mmap) + self.assertEqual(95, cs.get_calculated(mmap)) + + def test_with_MemoryMap(self): + mmap = memmap.MemoryMap('...\x2A') + self._test_checksum(mmap) + + def test_with_MemoryMapBytes(self): + mmap = memmap.MemoryMapBytes(bytes(b'...\x2A')) + self._test_checksum(mmap) + + def test_with_bytes(self): + self._test_checksum(b'...\x2A') + + def test_with_str(self): + self._test_checksum('...\x2A') -- cgit v1.2.3