aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py151
-rw-r--r--tests/icom_clone_simulator.py195
-rw-r--r--tests/images/Alinco_DJ-G7EG.imgbin0 -> 108480 bytes
-rw-r--r--tests/images/Alinco_DJ175.imgbin0 -> 6896 bytes
-rw-r--r--tests/images/Alinco_DJ596.imgbin0 -> 4096 bytes
-rw-r--r--tests/images/Alinco_DR235T.imgbin0 -> 4096 bytes
-rw-r--r--tests/images/AnyTone_OBLTR-8R.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/AnyTone_TERMN-8R.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/BTECH_GMRS-50X1.imgbin0 -> 16384 bytes
-rw-r--r--tests/images/BTECH_GMRS-V1.imgbin0 -> 8200 bytes
-rw-r--r--tests/images/BTECH_MURS-V1.imgbin0 -> 8200 bytes
-rwxr-xr-xtests/images/BTECH_UV-2501+220.imgbin0 -> 16384 bytes
-rwxr-xr-xtests/images/BTECH_UV-25X2.imgbin0 -> 16384 bytes
-rwxr-xr-xtests/images/BTECH_UV-25X4.imgbin0 -> 16384 bytes
-rwxr-xr-xtests/images/BTECH_UV-5001.imgbin0 -> 16384 bytes
-rwxr-xr-xtests/images/BTECH_UV-50X2.imgbin0 -> 16384 bytes
-rw-r--r--tests/images/BTECH_UV-50X3.imgbin0 -> 32768 bytes
-rwxr-xr-xtests/images/BTECH_UV-5X3.imgbin0 -> 8206 bytes
-rw-r--r--tests/images/Baofeng_BF-888.imgbin0 -> 992 bytes
-rw-r--r--tests/images/Baofeng_BF-A58S.imgbin0 -> 8361 bytes
-rw-r--r--tests/images/Baofeng_BF-T1.imgbin0 -> 2048 bytes
-rw-r--r--tests/images/Baofeng_F-11.imgbin0 -> 6472 bytes
-rw-r--r--tests/images/Baofeng_UV-3R.imgbin0 -> 3648 bytes
-rw-r--r--tests/images/Baofeng_UV-5R.imgbin0 -> 6472 bytes
-rwxr-xr-xtests/images/Baofeng_UV-6R.imgbin0 -> 8200 bytes
-rw-r--r--tests/images/Baofeng_UV-B5.imgbin0 -> 4144 bytes
-rw-r--r--tests/images/Baojie_BJ-9900.imgbin0 -> 6385 bytes
-rw-r--r--tests/images/Boblov_X3Plus.imgbin0 -> 1008 bytes
-rwxr-xr-xtests/images/Feidaxin_FD-268A.imgbin0 -> 2048 bytes
-rwxr-xr-xtests/images/Feidaxin_FD-268B.imgbin0 -> 2048 bytes
-rwxr-xr-xtests/images/Feidaxin_FD-288B.imgbin0 -> 2048 bytes
-rw-r--r--tests/images/Generic_CSV.csv104
-rw-r--r--tests/images/Icom_IC-208H.imgbin0 -> 9728 bytes
-rw-r--r--tests/images/Icom_IC-2100H.imgbin0 -> 2016 bytes
-rw-r--r--tests/images/Icom_IC-2200H.imgbin0 -> 6848 bytes
-rw-r--r--tests/images/Icom_IC-2300H.imgbin0 -> 6304 bytes
-rw-r--r--tests/images/Icom_IC-2720H.imgbin0 -> 5152 bytes
-rw-r--r--tests/images/Icom_IC-2730A.imgbin0 -> 21312 bytes
-rw-r--r--tests/images/Icom_IC-2820H.imgbin0 -> 44224 bytes
-rwxr-xr-xtests/images/Icom_IC-P7.imgbin0 -> 29952 bytes
-rw-r--r--tests/images/Icom_IC-Q7A.imgbin0 -> 1984 bytes
-rw-r--r--tests/images/Icom_IC-T70.imgbin0 -> 6624 bytes
-rw-r--r--tests/images/Icom_IC-T7H.imgbin0 -> 944 bytes
-rw-r--r--tests/images/Icom_IC-T8A.imgbin0 -> 1968 bytes
-rw-r--r--tests/images/Icom_IC-V82_U82.imgbin0 -> 6464 bytes
-rw-r--r--tests/images/Icom_IC-W32A.imgbin0 -> 4064 bytes
-rw-r--r--tests/images/Icom_IC-W32E.imgbin0 -> 4065 bytes
-rw-r--r--tests/images/Icom_ID-31A.imgbin0 -> 87296 bytes
-rw-r--r--tests/images/Icom_ID-51.imgbin0 -> 129856 bytes
-rwxr-xr-xtests/images/Icom_ID-51_Plus.imgbin0 -> 129856 bytes
-rw-r--r--tests/images/Icom_ID-800H_v2.imgbin0 -> 14528 bytes
-rw-r--r--tests/images/Icom_ID-880H.imgbin0 -> 62976 bytes
-rw-r--r--tests/images/Jetstream_JT220M.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Jetstream_JT270M.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Jetstream_JT270MH.imgbin0 -> 8192 bytes
-rwxr-xr-xtests/images/KYD_IP-620.imgbin0 -> 8192 bytes
-rwxr-xr-xtests/images/KYD_NC-630A.imgbin0 -> 824 bytes
-rw-r--r--tests/images/Kenwood_HMK.hmk71
-rw-r--r--tests/images/Kenwood_TH-D72_clone_mode.imgbin0 -> 65536 bytes
-rwxr-xr-xtests/images/Kenwood_TK-272G.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Kenwood_TK-3180K2.imgbin0 -> 53681 bytes
-rwxr-xr-xtests/images/Kenwood_TK-760G.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Kenwood_TK-8102.imgbin0 -> 1040 bytes
-rw-r--r--tests/images/Kenwood_TK-8180.imgbin0 -> 53673 bytes
-rw-r--r--tests/images/Kenwood_TS-480_CloneMode.imgbin0 -> 3018 bytes
-rwxr-xr-xtests/images/LUITON_LT-725UV.imgbin0 -> 7176 bytes
-rw-r--r--tests/images/Leixen_VV-898.imgbin0 -> 8192 bytes
-rwxr-xr-xtests/images/Leixen_VV-898S.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Polmar_DB-50M.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Puxing_PX-2R.imgbin0 -> 4064 bytes
-rw-r--r--tests/images/Puxing_PX-777.imgbin0 -> 3168 bytes
-rw-r--r--tests/images/Puxing_PX-888K.imgbin0 -> 4096 bytes
-rwxr-xr-xtests/images/QYT_KT7900D.imgbin0 -> 16384 bytes
-rwxr-xr-xtests/images/QYT_KT8900D.imgbin0 -> 16384 bytes
-rw-r--r--tests/images/Radioddity_R2.imgbin0 -> 1181 bytes
-rwxr-xr-xtests/images/Radtel_T18.imgbin0 -> 1008 bytes
-rw-r--r--tests/images/Retevis_RT21.imgbin0 -> 1024 bytes
-rw-r--r--tests/images/Retevis_RT22.imgbin0 -> 1032 bytes
-rwxr-xr-xtests/images/Retevis_RT23.imgbin0 -> 4096 bytes
-rw-r--r--tests/images/Retevis_RT26.imgbin0 -> 1024 bytes
-rwxr-xr-xtests/images/TDXone_TD-Q8A.imgbin0 -> 8200 bytes
-rw-r--r--tests/images/TYT_TH-350.imgbin0 -> 4297 bytes
-rw-r--r--tests/images/TYT_TH-7800.imgbin0 -> 65296 bytes
-rw-r--r--tests/images/TYT_TH-9800.imgbin0 -> 65296 bytes
-rwxr-xr-xtests/images/TYT_TH-UV3R-25.imgbin0 -> 2864 bytes
-rw-r--r--tests/images/TYT_TH-UV3R.imgbin0 -> 2320 bytes
-rw-r--r--tests/images/TYT_TH-UV8000.imgbin0 -> 5040 bytes
-rw-r--r--tests/images/TYT_TH-UVF1.imgbin0 -> 4112 bytes
-rw-r--r--tests/images/TYT_TH9000_144.imgbin0 -> 16384 bytes
-rw-r--r--tests/images/Vertex_Standard_VXA-700.imgbin0 -> 4096 bytes
-rwxr-xr-xtests/images/WACCOM_MINI-8900.imgbin0 -> 16384 bytes
-rw-r--r--tests/images/Wouxun_KG-816.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Wouxun_KG-818.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Wouxun_KG-UV6.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Wouxun_KG-UV8D.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Wouxun_KG-UV8D_Plus.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Wouxun_KG-UV8E.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Wouxun_KG-UV9D_Plus.imgbin0 -> 32768 bytes
-rw-r--r--tests/images/Wouxun_KG-UVD1P.imgbin0 -> 8192 bytes
-rw-r--r--tests/images/Yaesu_FT-1500M.imgbin0 -> 4140 bytes
-rw-r--r--tests/images/Yaesu_FT-1802M.imgbin0 -> 8011 bytes
-rw-r--r--tests/images/Yaesu_FT-1D_R.imgbin0 -> 130507 bytes
-rw-r--r--tests/images/Yaesu_FT-25R.imgbin0 -> 8689 bytes
-rw-r--r--tests/images/Yaesu_FT-2800M.imgbin0 -> 7680 bytes
-rwxr-xr-xtests/images/Yaesu_FT-2900R_1900R.imgbin0 -> 8000 bytes
-rw-r--r--tests/images/Yaesu_FT-450D.imgbin0 -> 15024 bytes
-rw-r--r--tests/images/Yaesu_FT-4VR.imgbin0 -> 8697 bytes
-rw-r--r--tests/images/Yaesu_FT-4XE.imgbin0 -> 8697 bytes
-rw-r--r--tests/images/Yaesu_FT-4XR.imgbin0 -> 8689 bytes
-rwxr-xr-xtests/images/Yaesu_FT-50.imgbin0 -> 3723 bytes
-rw-r--r--tests/images/Yaesu_FT-60.imgbin0 -> 28617 bytes
-rw-r--r--tests/images/Yaesu_FT-65E.imgbin0 -> 8697 bytes
-rw-r--r--tests/images/Yaesu_FT-65R.imgbin0 -> 8693 bytes
-rw-r--r--tests/images/Yaesu_FT-70D.imgbin0 -> 65227 bytes
-rw-r--r--tests/images/Yaesu_FT-7100M.imgbin0 -> 7936 bytes
-rw-r--r--tests/images/Yaesu_FT-7800_7900.imgbin0 -> 31561 bytes
-rw-r--r--tests/images/Yaesu_FT-817.imgbin0 -> 6509 bytes
-rw-r--r--tests/images/Yaesu_FT-817ND.imgbin0 -> 6521 bytes
-rw-r--r--tests/images/Yaesu_FT-817ND_US.imgbin0 -> 6651 bytes
-rw-r--r--tests/images/Yaesu_FT-818.imgbin0 -> 6730 bytes
-rw-r--r--tests/images/Yaesu_FT-857_897.imgbin0 -> 7341 bytes
-rw-r--r--tests/images/Yaesu_FT-857_897_US.imgbin0 -> 7481 bytes
-rw-r--r--tests/images/Yaesu_FT-8800.imgbin0 -> 22217 bytes
-rw-r--r--tests/images/Yaesu_FT-8900.imgbin0 -> 14793 bytes
-rw-r--r--tests/images/Yaesu_FT2D_R.imgbin0 -> 130507 bytes
-rw-r--r--tests/images/Yaesu_FT3D_R.imgbin0 -> 130652 bytes
-rw-r--r--tests/images/Yaesu_FTM-3200D_R.imgbin0 -> 65227 bytes
-rw-r--r--tests/images/Yaesu_FTM-350.imgbin0 -> 65664 bytes
-rw-r--r--tests/images/Yaesu_VX-2.imgbin0 -> 32595 bytes
-rw-r--r--tests/images/Yaesu_VX-3.imgbin0 -> 32587 bytes
-rw-r--r--tests/images/Yaesu_VX-5.imgbin0 -> 8123 bytes
-rw-r--r--tests/images/Yaesu_VX-6.imgbin0 -> 32587 bytes
-rw-r--r--tests/images/Yaesu_VX-7.imgbin0 -> 16211 bytes
-rw-r--r--tests/images/Yaesu_VX-8DR.imgbin0 -> 65227 bytes
-rw-r--r--tests/images/Yaesu_VX-8GE.imgbin0 -> 65227 bytes
-rw-r--r--tests/images/Yaesu_VX-8R.imgbin0 -> 65227 bytes
-rwxr-xr-xtests/run_tests3
-rwxr-xr-xtests/run_tests.py1372
-rw-r--r--tests/test_drivers.py24
-rw-r--r--tests/unit/__init__.py0
-rw-r--r--tests/unit/base.py50
-rw-r--r--tests/unit/test_bitwise.py341
-rw-r--r--tests/unit/test_chirp_common.py415
-rw-r--r--tests/unit/test_directory.py69
-rw-r--r--tests/unit/test_icom_clone.py117
-rw-r--r--tests/unit/test_import_logic.py373
-rw-r--r--tests/unit/test_mappingmodel.py283
-rw-r--r--tests/unit/test_memedit_edits.py82
-rw-r--r--tests/unit/test_platform.py62
-rw-r--r--tests/unit/test_repeaterbook.py28
-rw-r--r--tests/unit/test_settings.py140
-rw-r--r--tests/unit/test_shiftdialog.py112
-rw-r--r--tests/unit/test_utils.py21
-rw-r--r--tests/unit/test_yaesu_clone.py41
154 files changed, 4054 insertions, 0 deletions
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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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
--- /dev/null
+++ b/tests/images/Alinco_DJ-G7EG.img
Binary files differ
diff --git a/tests/images/Alinco_DJ175.img b/tests/images/Alinco_DJ175.img
new file mode 100644
index 0000000..0c05432
--- /dev/null
+++ b/tests/images/Alinco_DJ175.img
Binary files differ
diff --git a/tests/images/Alinco_DJ596.img b/tests/images/Alinco_DJ596.img
new file mode 100644
index 0000000..d37be19
--- /dev/null
+++ b/tests/images/Alinco_DJ596.img
Binary files differ
diff --git a/tests/images/Alinco_DR235T.img b/tests/images/Alinco_DR235T.img
new file mode 100644
index 0000000..8256271
--- /dev/null
+++ b/tests/images/Alinco_DR235T.img
Binary files differ
diff --git a/tests/images/AnyTone_OBLTR-8R.img b/tests/images/AnyTone_OBLTR-8R.img
new file mode 100644
index 0000000..2f11032
--- /dev/null
+++ b/tests/images/AnyTone_OBLTR-8R.img
Binary files differ
diff --git a/tests/images/AnyTone_TERMN-8R.img b/tests/images/AnyTone_TERMN-8R.img
new file mode 100644
index 0000000..0c88931
--- /dev/null
+++ b/tests/images/AnyTone_TERMN-8R.img
Binary files differ
diff --git a/tests/images/BTECH_GMRS-50X1.img b/tests/images/BTECH_GMRS-50X1.img
new file mode 100644
index 0000000..e84780f
--- /dev/null
+++ b/tests/images/BTECH_GMRS-50X1.img
Binary files differ
diff --git a/tests/images/BTECH_GMRS-V1.img b/tests/images/BTECH_GMRS-V1.img
new file mode 100644
index 0000000..afc0a36
--- /dev/null
+++ b/tests/images/BTECH_GMRS-V1.img
Binary files differ
diff --git a/tests/images/BTECH_MURS-V1.img b/tests/images/BTECH_MURS-V1.img
new file mode 100644
index 0000000..fac635c
--- /dev/null
+++ b/tests/images/BTECH_MURS-V1.img
Binary files 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
--- /dev/null
+++ b/tests/images/BTECH_UV-2501+220.img
Binary files differ
diff --git a/tests/images/BTECH_UV-25X2.img b/tests/images/BTECH_UV-25X2.img
new file mode 100755
index 0000000..aec97d8
--- /dev/null
+++ b/tests/images/BTECH_UV-25X2.img
Binary files differ
diff --git a/tests/images/BTECH_UV-25X4.img b/tests/images/BTECH_UV-25X4.img
new file mode 100755
index 0000000..12ce9f9
--- /dev/null
+++ b/tests/images/BTECH_UV-25X4.img
Binary files differ
diff --git a/tests/images/BTECH_UV-5001.img b/tests/images/BTECH_UV-5001.img
new file mode 100755
index 0000000..377be8b
--- /dev/null
+++ b/tests/images/BTECH_UV-5001.img
Binary files differ
diff --git a/tests/images/BTECH_UV-50X2.img b/tests/images/BTECH_UV-50X2.img
new file mode 100755
index 0000000..6bac4fb
--- /dev/null
+++ b/tests/images/BTECH_UV-50X2.img
Binary files differ
diff --git a/tests/images/BTECH_UV-50X3.img b/tests/images/BTECH_UV-50X3.img
new file mode 100644
index 0000000..6da4293
--- /dev/null
+++ b/tests/images/BTECH_UV-50X3.img
Binary files differ
diff --git a/tests/images/BTECH_UV-5X3.img b/tests/images/BTECH_UV-5X3.img
new file mode 100755
index 0000000..aa6ad8a
--- /dev/null
+++ b/tests/images/BTECH_UV-5X3.img
Binary files differ
diff --git a/tests/images/Baofeng_BF-888.img b/tests/images/Baofeng_BF-888.img
new file mode 100644
index 0000000..0304ddf
--- /dev/null
+++ b/tests/images/Baofeng_BF-888.img
Binary files differ
diff --git a/tests/images/Baofeng_BF-A58S.img b/tests/images/Baofeng_BF-A58S.img
new file mode 100644
index 0000000..bbcbffc
--- /dev/null
+++ b/tests/images/Baofeng_BF-A58S.img
Binary files differ
diff --git a/tests/images/Baofeng_BF-T1.img b/tests/images/Baofeng_BF-T1.img
new file mode 100644
index 0000000..849896c
--- /dev/null
+++ b/tests/images/Baofeng_BF-T1.img
Binary files differ
diff --git a/tests/images/Baofeng_F-11.img b/tests/images/Baofeng_F-11.img
new file mode 100644
index 0000000..b13f9b4
--- /dev/null
+++ b/tests/images/Baofeng_F-11.img
Binary files differ
diff --git a/tests/images/Baofeng_UV-3R.img b/tests/images/Baofeng_UV-3R.img
new file mode 100644
index 0000000..8333f02
--- /dev/null
+++ b/tests/images/Baofeng_UV-3R.img
Binary files differ
diff --git a/tests/images/Baofeng_UV-5R.img b/tests/images/Baofeng_UV-5R.img
new file mode 100644
index 0000000..dcdc3e8
--- /dev/null
+++ b/tests/images/Baofeng_UV-5R.img
Binary files differ
diff --git a/tests/images/Baofeng_UV-6R.img b/tests/images/Baofeng_UV-6R.img
new file mode 100755
index 0000000..32f5c15
--- /dev/null
+++ b/tests/images/Baofeng_UV-6R.img
Binary files differ
diff --git a/tests/images/Baofeng_UV-B5.img b/tests/images/Baofeng_UV-B5.img
new file mode 100644
index 0000000..5d69456
--- /dev/null
+++ b/tests/images/Baofeng_UV-B5.img
Binary files differ
diff --git a/tests/images/Baojie_BJ-9900.img b/tests/images/Baojie_BJ-9900.img
new file mode 100644
index 0000000..905dda3
--- /dev/null
+++ b/tests/images/Baojie_BJ-9900.img
Binary files differ
diff --git a/tests/images/Boblov_X3Plus.img b/tests/images/Boblov_X3Plus.img
new file mode 100644
index 0000000..07331e4
--- /dev/null
+++ b/tests/images/Boblov_X3Plus.img
Binary files differ
diff --git a/tests/images/Feidaxin_FD-268A.img b/tests/images/Feidaxin_FD-268A.img
new file mode 100755
index 0000000..52fc11b
--- /dev/null
+++ b/tests/images/Feidaxin_FD-268A.img
Binary files differ
diff --git a/tests/images/Feidaxin_FD-268B.img b/tests/images/Feidaxin_FD-268B.img
new file mode 100755
index 0000000..8de4949
--- /dev/null
+++ b/tests/images/Feidaxin_FD-268B.img
Binary files differ
diff --git a/tests/images/Feidaxin_FD-288B.img b/tests/images/Feidaxin_FD-288B.img
new file mode 100755
index 0000000..778ff91
--- /dev/null
+++ b/tests/images/Feidaxin_FD-288B.img
Binary files 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
--- /dev/null
+++ b/tests/images/Icom_IC-208H.img
Binary files differ
diff --git a/tests/images/Icom_IC-2100H.img b/tests/images/Icom_IC-2100H.img
new file mode 100644
index 0000000..cf6ffd8
--- /dev/null
+++ b/tests/images/Icom_IC-2100H.img
Binary files differ
diff --git a/tests/images/Icom_IC-2200H.img b/tests/images/Icom_IC-2200H.img
new file mode 100644
index 0000000..56ee266
--- /dev/null
+++ b/tests/images/Icom_IC-2200H.img
Binary files differ
diff --git a/tests/images/Icom_IC-2300H.img b/tests/images/Icom_IC-2300H.img
new file mode 100644
index 0000000..cd6aad0
--- /dev/null
+++ b/tests/images/Icom_IC-2300H.img
Binary files differ
diff --git a/tests/images/Icom_IC-2720H.img b/tests/images/Icom_IC-2720H.img
new file mode 100644
index 0000000..e153ce1
--- /dev/null
+++ b/tests/images/Icom_IC-2720H.img
Binary files differ
diff --git a/tests/images/Icom_IC-2730A.img b/tests/images/Icom_IC-2730A.img
new file mode 100644
index 0000000..73b973a
--- /dev/null
+++ b/tests/images/Icom_IC-2730A.img
Binary files differ
diff --git a/tests/images/Icom_IC-2820H.img b/tests/images/Icom_IC-2820H.img
new file mode 100644
index 0000000..e845f9f
--- /dev/null
+++ b/tests/images/Icom_IC-2820H.img
Binary files differ
diff --git a/tests/images/Icom_IC-P7.img b/tests/images/Icom_IC-P7.img
new file mode 100755
index 0000000..35a0966
--- /dev/null
+++ b/tests/images/Icom_IC-P7.img
Binary files differ
diff --git a/tests/images/Icom_IC-Q7A.img b/tests/images/Icom_IC-Q7A.img
new file mode 100644
index 0000000..bad5f87
--- /dev/null
+++ b/tests/images/Icom_IC-Q7A.img
Binary files differ
diff --git a/tests/images/Icom_IC-T70.img b/tests/images/Icom_IC-T70.img
new file mode 100644
index 0000000..5c09019
--- /dev/null
+++ b/tests/images/Icom_IC-T70.img
Binary files differ
diff --git a/tests/images/Icom_IC-T7H.img b/tests/images/Icom_IC-T7H.img
new file mode 100644
index 0000000..5186620
--- /dev/null
+++ b/tests/images/Icom_IC-T7H.img
Binary files differ
diff --git a/tests/images/Icom_IC-T8A.img b/tests/images/Icom_IC-T8A.img
new file mode 100644
index 0000000..c5bcc45
--- /dev/null
+++ b/tests/images/Icom_IC-T8A.img
Binary files 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
--- /dev/null
+++ b/tests/images/Icom_IC-V82_U82.img
Binary files differ
diff --git a/tests/images/Icom_IC-W32A.img b/tests/images/Icom_IC-W32A.img
new file mode 100644
index 0000000..b5b239c
--- /dev/null
+++ b/tests/images/Icom_IC-W32A.img
Binary files differ
diff --git a/tests/images/Icom_IC-W32E.img b/tests/images/Icom_IC-W32E.img
new file mode 100644
index 0000000..32bee84
--- /dev/null
+++ b/tests/images/Icom_IC-W32E.img
Binary files differ
diff --git a/tests/images/Icom_ID-31A.img b/tests/images/Icom_ID-31A.img
new file mode 100644
index 0000000..afdebdd
--- /dev/null
+++ b/tests/images/Icom_ID-31A.img
Binary files differ
diff --git a/tests/images/Icom_ID-51.img b/tests/images/Icom_ID-51.img
new file mode 100644
index 0000000..399c857
--- /dev/null
+++ b/tests/images/Icom_ID-51.img
Binary files 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
--- /dev/null
+++ b/tests/images/Icom_ID-51_Plus.img
Binary files 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
--- /dev/null
+++ b/tests/images/Icom_ID-800H_v2.img
Binary files differ
diff --git a/tests/images/Icom_ID-880H.img b/tests/images/Icom_ID-880H.img
new file mode 100644
index 0000000..0a83f24
--- /dev/null
+++ b/tests/images/Icom_ID-880H.img
Binary files differ
diff --git a/tests/images/Jetstream_JT220M.img b/tests/images/Jetstream_JT220M.img
new file mode 100644
index 0000000..bb0b4a7
--- /dev/null
+++ b/tests/images/Jetstream_JT220M.img
Binary files differ
diff --git a/tests/images/Jetstream_JT270M.img b/tests/images/Jetstream_JT270M.img
new file mode 100644
index 0000000..acc95d4
--- /dev/null
+++ b/tests/images/Jetstream_JT270M.img
Binary files differ
diff --git a/tests/images/Jetstream_JT270MH.img b/tests/images/Jetstream_JT270MH.img
new file mode 100644
index 0000000..4027cb0
--- /dev/null
+++ b/tests/images/Jetstream_JT270MH.img
Binary files differ
diff --git a/tests/images/KYD_IP-620.img b/tests/images/KYD_IP-620.img
new file mode 100755
index 0000000..c088dfe
--- /dev/null
+++ b/tests/images/KYD_IP-620.img
Binary files differ
diff --git a/tests/images/KYD_NC-630A.img b/tests/images/KYD_NC-630A.img
new file mode 100755
index 0000000..2c627fa
--- /dev/null
+++ b/tests/images/KYD_NC-630A.img
Binary files 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
--- /dev/null
+++ b/tests/images/Kenwood_TH-D72_clone_mode.img
Binary files differ
diff --git a/tests/images/Kenwood_TK-272G.img b/tests/images/Kenwood_TK-272G.img
new file mode 100755
index 0000000..bdeed1f
--- /dev/null
+++ b/tests/images/Kenwood_TK-272G.img
Binary files differ
diff --git a/tests/images/Kenwood_TK-3180K2.img b/tests/images/Kenwood_TK-3180K2.img
new file mode 100644
index 0000000..b3524c9
--- /dev/null
+++ b/tests/images/Kenwood_TK-3180K2.img
Binary files differ
diff --git a/tests/images/Kenwood_TK-760G.img b/tests/images/Kenwood_TK-760G.img
new file mode 100755
index 0000000..23c883a
--- /dev/null
+++ b/tests/images/Kenwood_TK-760G.img
Binary files differ
diff --git a/tests/images/Kenwood_TK-8102.img b/tests/images/Kenwood_TK-8102.img
new file mode 100644
index 0000000..d03fbc4
--- /dev/null
+++ b/tests/images/Kenwood_TK-8102.img
Binary files differ
diff --git a/tests/images/Kenwood_TK-8180.img b/tests/images/Kenwood_TK-8180.img
new file mode 100644
index 0000000..06a5910
--- /dev/null
+++ b/tests/images/Kenwood_TK-8180.img
Binary files 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
--- /dev/null
+++ b/tests/images/Kenwood_TS-480_CloneMode.img
Binary files differ
diff --git a/tests/images/LUITON_LT-725UV.img b/tests/images/LUITON_LT-725UV.img
new file mode 100755
index 0000000..537e13b
--- /dev/null
+++ b/tests/images/LUITON_LT-725UV.img
Binary files differ
diff --git a/tests/images/Leixen_VV-898.img b/tests/images/Leixen_VV-898.img
new file mode 100644
index 0000000..abeec14
--- /dev/null
+++ b/tests/images/Leixen_VV-898.img
Binary files differ
diff --git a/tests/images/Leixen_VV-898S.img b/tests/images/Leixen_VV-898S.img
new file mode 100755
index 0000000..10a264a
--- /dev/null
+++ b/tests/images/Leixen_VV-898S.img
Binary files differ
diff --git a/tests/images/Polmar_DB-50M.img b/tests/images/Polmar_DB-50M.img
new file mode 100644
index 0000000..04df657
--- /dev/null
+++ b/tests/images/Polmar_DB-50M.img
Binary files differ
diff --git a/tests/images/Puxing_PX-2R.img b/tests/images/Puxing_PX-2R.img
new file mode 100644
index 0000000..63c615c
--- /dev/null
+++ b/tests/images/Puxing_PX-2R.img
Binary files differ
diff --git a/tests/images/Puxing_PX-777.img b/tests/images/Puxing_PX-777.img
new file mode 100644
index 0000000..3fceb43
--- /dev/null
+++ b/tests/images/Puxing_PX-777.img
Binary files differ
diff --git a/tests/images/Puxing_PX-888K.img b/tests/images/Puxing_PX-888K.img
new file mode 100644
index 0000000..27d30d0
--- /dev/null
+++ b/tests/images/Puxing_PX-888K.img
Binary files differ
diff --git a/tests/images/QYT_KT7900D.img b/tests/images/QYT_KT7900D.img
new file mode 100755
index 0000000..73bfc37
--- /dev/null
+++ b/tests/images/QYT_KT7900D.img
Binary files differ
diff --git a/tests/images/QYT_KT8900D.img b/tests/images/QYT_KT8900D.img
new file mode 100755
index 0000000..e31310f
--- /dev/null
+++ b/tests/images/QYT_KT8900D.img
Binary files differ
diff --git a/tests/images/Radioddity_R2.img b/tests/images/Radioddity_R2.img
new file mode 100644
index 0000000..0bbd846
--- /dev/null
+++ b/tests/images/Radioddity_R2.img
Binary files differ
diff --git a/tests/images/Radtel_T18.img b/tests/images/Radtel_T18.img
new file mode 100755
index 0000000..f508675
--- /dev/null
+++ b/tests/images/Radtel_T18.img
Binary files differ
diff --git a/tests/images/Retevis_RT21.img b/tests/images/Retevis_RT21.img
new file mode 100644
index 0000000..030d4f7
--- /dev/null
+++ b/tests/images/Retevis_RT21.img
Binary files differ
diff --git a/tests/images/Retevis_RT22.img b/tests/images/Retevis_RT22.img
new file mode 100644
index 0000000..ffca933
--- /dev/null
+++ b/tests/images/Retevis_RT22.img
Binary files differ
diff --git a/tests/images/Retevis_RT23.img b/tests/images/Retevis_RT23.img
new file mode 100755
index 0000000..2bb0252
--- /dev/null
+++ b/tests/images/Retevis_RT23.img
Binary files differ
diff --git a/tests/images/Retevis_RT26.img b/tests/images/Retevis_RT26.img
new file mode 100644
index 0000000..fdb6141
--- /dev/null
+++ b/tests/images/Retevis_RT26.img
Binary files differ
diff --git a/tests/images/TDXone_TD-Q8A.img b/tests/images/TDXone_TD-Q8A.img
new file mode 100755
index 0000000..6bd3d07
--- /dev/null
+++ b/tests/images/TDXone_TD-Q8A.img
Binary files differ
diff --git a/tests/images/TYT_TH-350.img b/tests/images/TYT_TH-350.img
new file mode 100644
index 0000000..7d1fd3c
--- /dev/null
+++ b/tests/images/TYT_TH-350.img
Binary files differ
diff --git a/tests/images/TYT_TH-7800.img b/tests/images/TYT_TH-7800.img
new file mode 100644
index 0000000..5c9240d
--- /dev/null
+++ b/tests/images/TYT_TH-7800.img
Binary files differ
diff --git a/tests/images/TYT_TH-9800.img b/tests/images/TYT_TH-9800.img
new file mode 100644
index 0000000..a2f584b
--- /dev/null
+++ b/tests/images/TYT_TH-9800.img
Binary files 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
--- /dev/null
+++ b/tests/images/TYT_TH-UV3R-25.img
Binary files differ
diff --git a/tests/images/TYT_TH-UV3R.img b/tests/images/TYT_TH-UV3R.img
new file mode 100644
index 0000000..75b2cf6
--- /dev/null
+++ b/tests/images/TYT_TH-UV3R.img
Binary files differ
diff --git a/tests/images/TYT_TH-UV8000.img b/tests/images/TYT_TH-UV8000.img
new file mode 100644
index 0000000..71b7aa9
--- /dev/null
+++ b/tests/images/TYT_TH-UV8000.img
Binary files differ
diff --git a/tests/images/TYT_TH-UVF1.img b/tests/images/TYT_TH-UVF1.img
new file mode 100644
index 0000000..f01edd6
--- /dev/null
+++ b/tests/images/TYT_TH-UVF1.img
Binary files differ
diff --git a/tests/images/TYT_TH9000_144.img b/tests/images/TYT_TH9000_144.img
new file mode 100644
index 0000000..871e986
--- /dev/null
+++ b/tests/images/TYT_TH9000_144.img
Binary files 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
--- /dev/null
+++ b/tests/images/Vertex_Standard_VXA-700.img
Binary files differ
diff --git a/tests/images/WACCOM_MINI-8900.img b/tests/images/WACCOM_MINI-8900.img
new file mode 100755
index 0000000..9b5737a
--- /dev/null
+++ b/tests/images/WACCOM_MINI-8900.img
Binary files differ
diff --git a/tests/images/Wouxun_KG-816.img b/tests/images/Wouxun_KG-816.img
new file mode 100644
index 0000000..090e18d
--- /dev/null
+++ b/tests/images/Wouxun_KG-816.img
Binary files differ
diff --git a/tests/images/Wouxun_KG-818.img b/tests/images/Wouxun_KG-818.img
new file mode 100644
index 0000000..716da73
--- /dev/null
+++ b/tests/images/Wouxun_KG-818.img
Binary files differ
diff --git a/tests/images/Wouxun_KG-UV6.img b/tests/images/Wouxun_KG-UV6.img
new file mode 100644
index 0000000..b1a7795
--- /dev/null
+++ b/tests/images/Wouxun_KG-UV6.img
Binary files differ
diff --git a/tests/images/Wouxun_KG-UV8D.img b/tests/images/Wouxun_KG-UV8D.img
new file mode 100644
index 0000000..1c66447
--- /dev/null
+++ b/tests/images/Wouxun_KG-UV8D.img
Binary files 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
--- /dev/null
+++ b/tests/images/Wouxun_KG-UV8D_Plus.img
Binary files differ
diff --git a/tests/images/Wouxun_KG-UV8E.img b/tests/images/Wouxun_KG-UV8E.img
new file mode 100644
index 0000000..89e5e1f
--- /dev/null
+++ b/tests/images/Wouxun_KG-UV8E.img
Binary files 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
--- /dev/null
+++ b/tests/images/Wouxun_KG-UV9D_Plus.img
Binary files differ
diff --git a/tests/images/Wouxun_KG-UVD1P.img b/tests/images/Wouxun_KG-UVD1P.img
new file mode 100644
index 0000000..93df646
--- /dev/null
+++ b/tests/images/Wouxun_KG-UVD1P.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-1500M.img b/tests/images/Yaesu_FT-1500M.img
new file mode 100644
index 0000000..581e353
--- /dev/null
+++ b/tests/images/Yaesu_FT-1500M.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-1802M.img b/tests/images/Yaesu_FT-1802M.img
new file mode 100644
index 0000000..f13e34d
--- /dev/null
+++ b/tests/images/Yaesu_FT-1802M.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FT-1D_R.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-25R.img b/tests/images/Yaesu_FT-25R.img
new file mode 100644
index 0000000..fef4b0e
--- /dev/null
+++ b/tests/images/Yaesu_FT-25R.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-2800M.img b/tests/images/Yaesu_FT-2800M.img
new file mode 100644
index 0000000..35ccf18
--- /dev/null
+++ b/tests/images/Yaesu_FT-2800M.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FT-2900R_1900R.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-450D.img b/tests/images/Yaesu_FT-450D.img
new file mode 100644
index 0000000..09873ea
--- /dev/null
+++ b/tests/images/Yaesu_FT-450D.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-4VR.img b/tests/images/Yaesu_FT-4VR.img
new file mode 100644
index 0000000..ffa6cfe
--- /dev/null
+++ b/tests/images/Yaesu_FT-4VR.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-4XE.img b/tests/images/Yaesu_FT-4XE.img
new file mode 100644
index 0000000..fb8087a
--- /dev/null
+++ b/tests/images/Yaesu_FT-4XE.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-4XR.img b/tests/images/Yaesu_FT-4XR.img
new file mode 100644
index 0000000..b45d1ae
--- /dev/null
+++ b/tests/images/Yaesu_FT-4XR.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-50.img b/tests/images/Yaesu_FT-50.img
new file mode 100755
index 0000000..77cffd7
--- /dev/null
+++ b/tests/images/Yaesu_FT-50.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-60.img b/tests/images/Yaesu_FT-60.img
new file mode 100644
index 0000000..f17d470
--- /dev/null
+++ b/tests/images/Yaesu_FT-60.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-65E.img b/tests/images/Yaesu_FT-65E.img
new file mode 100644
index 0000000..94cda1a
--- /dev/null
+++ b/tests/images/Yaesu_FT-65E.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-65R.img b/tests/images/Yaesu_FT-65R.img
new file mode 100644
index 0000000..c62c3df
--- /dev/null
+++ b/tests/images/Yaesu_FT-65R.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-70D.img b/tests/images/Yaesu_FT-70D.img
new file mode 100644
index 0000000..b557586
--- /dev/null
+++ b/tests/images/Yaesu_FT-70D.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-7100M.img b/tests/images/Yaesu_FT-7100M.img
new file mode 100644
index 0000000..30e18ce
--- /dev/null
+++ b/tests/images/Yaesu_FT-7100M.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FT-7800_7900.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-817.img b/tests/images/Yaesu_FT-817.img
new file mode 100644
index 0000000..00e624d
--- /dev/null
+++ b/tests/images/Yaesu_FT-817.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-817ND.img b/tests/images/Yaesu_FT-817ND.img
new file mode 100644
index 0000000..c0a2bb4
--- /dev/null
+++ b/tests/images/Yaesu_FT-817ND.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FT-817ND_US.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-818.img b/tests/images/Yaesu_FT-818.img
new file mode 100644
index 0000000..59b6e62
--- /dev/null
+++ b/tests/images/Yaesu_FT-818.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FT-857_897.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FT-857_897_US.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-8800.img b/tests/images/Yaesu_FT-8800.img
new file mode 100644
index 0000000..4cbb883
--- /dev/null
+++ b/tests/images/Yaesu_FT-8800.img
Binary files differ
diff --git a/tests/images/Yaesu_FT-8900.img b/tests/images/Yaesu_FT-8900.img
new file mode 100644
index 0000000..0445467
--- /dev/null
+++ b/tests/images/Yaesu_FT-8900.img
Binary files differ
diff --git a/tests/images/Yaesu_FT2D_R.img b/tests/images/Yaesu_FT2D_R.img
new file mode 100644
index 0000000..459b0ce
--- /dev/null
+++ b/tests/images/Yaesu_FT2D_R.img
Binary files differ
diff --git a/tests/images/Yaesu_FT3D_R.img b/tests/images/Yaesu_FT3D_R.img
new file mode 100644
index 0000000..179d69e
--- /dev/null
+++ b/tests/images/Yaesu_FT3D_R.img
Binary files 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
--- /dev/null
+++ b/tests/images/Yaesu_FTM-3200D_R.img
Binary files differ
diff --git a/tests/images/Yaesu_FTM-350.img b/tests/images/Yaesu_FTM-350.img
new file mode 100644
index 0000000..1e19c47
--- /dev/null
+++ b/tests/images/Yaesu_FTM-350.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-2.img b/tests/images/Yaesu_VX-2.img
new file mode 100644
index 0000000..bc237fd
--- /dev/null
+++ b/tests/images/Yaesu_VX-2.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-3.img b/tests/images/Yaesu_VX-3.img
new file mode 100644
index 0000000..eedefd3
--- /dev/null
+++ b/tests/images/Yaesu_VX-3.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-5.img b/tests/images/Yaesu_VX-5.img
new file mode 100644
index 0000000..522c0f3
--- /dev/null
+++ b/tests/images/Yaesu_VX-5.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-6.img b/tests/images/Yaesu_VX-6.img
new file mode 100644
index 0000000..509099d
--- /dev/null
+++ b/tests/images/Yaesu_VX-6.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-7.img b/tests/images/Yaesu_VX-7.img
new file mode 100644
index 0000000..2542f8e
--- /dev/null
+++ b/tests/images/Yaesu_VX-7.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-8DR.img b/tests/images/Yaesu_VX-8DR.img
new file mode 100644
index 0000000..299fdc1
--- /dev/null
+++ b/tests/images/Yaesu_VX-8DR.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-8GE.img b/tests/images/Yaesu_VX-8GE.img
new file mode 100644
index 0000000..a140121
--- /dev/null
+++ b/tests/images/Yaesu_VX-8GE.img
Binary files differ
diff --git a/tests/images/Yaesu_VX-8R.img b/tests/images/Yaesu_VX-8R.img
new file mode 100644
index 0000000..0ae38f7
--- /dev/null
+++ b/tests/images/Yaesu_VX-8R.img
Binary files 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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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 = """
+<html>
+<head>
+<title>Test report for CHIRP version %s</title>
+<style>
+table.testlist {
+ border: thin solid black;
+ border-collapse: collapse;
+}
+td {
+ border: thin solid black;
+ padding: 2px;
+}
+th {
+ background-color: silver;
+ border: thin solid black;
+ padding: 2px;
+}
+td.PASSED {
+ background-color: green;
+}
+td.FAILED {
+ background-color: red;
+}
+td.CRASHED {
+ background-color: purple;
+}
+td.SKIPPED {
+ background-color: green;
+}
+</style>
+</head>
+<body>
+<h1>Test report for CHIRP version %s</h1>
+<h3>Generated on %s (%s)</h3>
+<table class="testlist">
+<tr>
+ <th>Vendor</th><th>Model</th><th>Test Case</th>
+ <th>Status</th><th>Message</th>
+</tr>
+""" % (CHIRP_VERSION, CHIRP_VERSION, time.strftime("%x at %X"), os.name)
+ print(s, file=self._out)
+
+ def cleanup(self):
+ print("</table></body>", file=self._out)
+ self._out.close()
+ print("Done")
+
+ def report(self, rclass, tc, msg, e):
+ s = ("<tr class='%s'>" % msg) + \
+ ("<td class='vendor'>%s</td>" % rclass.VENDOR) + \
+ ("<td class='model'>%s %s</td>" %
+ (rclass.MODEL, rclass.VARIANT)) + \
+ ("<td class='tc'>%s</td>" % tc) + \
+ ("<td class='%s'>%s</td>" % (msg, msg)) + \
+ ("<td class='error'>%s</td>" % e) + \
+ "</tr>"
+ 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
--- /dev/null
+++ b/tests/unit/__init__.py
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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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 <dsmith@danplanet.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+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')