From a999621703ea209eaf72f9699d005adf9449a17e Mon Sep 17 00:00:00 2001
From: Kien Le <kienle@passcal.nmt.edu>
Date: Mon, 10 Oct 2022 14:41:55 -0600
Subject: [PATCH] Add tests for functions in plottingData.py

---
 sohstationviewer/controller/plottingData.py |  22 +-
 tests/test_controller/test_plotting_data.py | 649 ++++++++++++++++++++
 2 files changed, 661 insertions(+), 10 deletions(-)
 create mode 100644 tests/test_controller/test_plotting_data.py

diff --git a/sohstationviewer/controller/plottingData.py b/sohstationviewer/controller/plottingData.py
index 098409ac0..a56137685 100755
--- a/sohstationviewer/controller/plottingData.py
+++ b/sohstationviewer/controller/plottingData.py
@@ -7,7 +7,6 @@ import math
 from obspy import UTCDateTime
 from sohstationviewer.conf import constants as const
 
-
 maxInt = 1E100
 maxFloat = 1.0E100
 
@@ -36,10 +35,12 @@ def getMassposValueColors(rangeOpt, chan_id, cMode, processing_log,
 
     if rangeOpt.lower() not in MassPosVoltRanges.keys():
         processing_log.append(
-            f"{chan_id}: The current selected Mass Position color range is "
-            f"'{rangeOpt}' isn't allowable. The accept ranges are: "
-            f"{', '.join(MassPosVoltRanges.keys())}",
-            "error"
+            (
+                f"{chan_id}: The current selected Mass Position color range is"
+                f" '{rangeOpt}' isn't allowable. The accept ranges are: "
+                f"{', '.join(MassPosVoltRanges.keys())}",
+                "error"
+            )
         )
         return
     massPosVoltRange = MassPosVoltRanges[rangeOpt]
@@ -54,7 +55,8 @@ def getMassposValueColors(rangeOpt, chan_id, cMode, processing_log,
         if i == len(massPosVoltRange) - 1:
             if retType == 'str':
                 valueColors.append(
-                    "%s:+%s" % (massPosVoltRange[i], massPosColorPallet[i+1]))
+                    "%s:+%s" % (
+                        massPosVoltRange[i], massPosColorPallet[i + 1]))
             else:
                 valueColors.append(
                     (massPosVoltRange[i], massPosColorPallet[i + 1]))
@@ -102,7 +104,7 @@ def getTitle(key, minTime, maxTime, dateMode):
     :return: title for the plot - str
     """
     diff = maxTime - minTime
-    hours = diff/3600
+    hours = diff / 3600
     return ("%s  %s  to  %s  (%s)" %
             (key,
              formatTime(minTime, dateMode, "HH:MM:SS"),
@@ -193,10 +195,10 @@ def getDayTicks():
     """
 
     times = list(range(const.NO_5M_1H, const.NO_5M_DAY, const.NO_5M_1H))
-    majorTimes = list(range(4*const.NO_5M_1H,
+    majorTimes = list(range(4 * const.NO_5M_1H,
                             const.NO_5M_DAY,
-                            4*const.NO_5M_1H))
-    majorTimeLabels = ["%02d" % int(t/const.NO_5M_1H) for t in majorTimes]
+                            4 * const.NO_5M_1H))
+    majorTimeLabels = ["%02d" % int(t / const.NO_5M_1H) for t in majorTimes]
     return times, majorTimes, majorTimeLabels
 
 
diff --git a/tests/test_controller/test_plotting_data.py b/tests/test_controller/test_plotting_data.py
new file mode 100644
index 000000000..c784da717
--- /dev/null
+++ b/tests/test_controller/test_plotting_data.py
@@ -0,0 +1,649 @@
+"""Tests for functions defined in sohstationviewer.controller.plottingData"""
+
+from unittest import TestCase
+
+from obspy import UTCDateTime
+
+from sohstationviewer.controller.plottingData import (
+    getMassposValueColors,
+    formatTime,
+    getTitle,
+    getGaps,
+    getTimeTicks,
+    getDayTicks,
+    getUnitBitweight
+)
+
+
+class TestGetGaps(TestCase):
+    """Test suite for getGaps."""
+    def test_mixed_gap_sizes(self):
+        """
+        Test getGaps - the given list of gaps contain both gaps that are too
+        short and gaps that are long enough.
+        """
+        gaps = [(0, 60), (60, 180), (180, 360)]
+        min_gap = 3
+        self.assertListEqual(getGaps(gaps, min_gap), [(180, 360)])
+
+    def test_empty_gap_list(self):
+        """
+        Test getGaps - the given list of gaps is empty.
+        """
+        gaps = []
+        min_gap = 3
+        self.assertListEqual(getGaps(gaps, min_gap), [])
+
+    def test_all_gaps_are_too_short(self):
+        """
+        Test getGaps - the given list of gaps only contain gaps that are too
+        short.
+        """
+        gaps = [(0, 60), (60, 180)]
+        min_gap = 3
+        self.assertListEqual(getGaps(gaps, min_gap), [])
+
+    def test_all_gaps_are_long_enough(self):
+        """
+        Test getGaps - the given list of gaps only contain gaps that are long
+        enough.
+        """
+        gaps = [(0, 180), (180, 360)]
+        min_gap = 3
+        self.assertListEqual(getGaps(gaps, min_gap), [(0, 180), (180, 360)])
+
+
+class TestGetDayTicks(TestCase):
+    """Test suite for getDayTicks."""
+    def test_get_day_ticks(self):
+        """Test getDayTicks."""
+        expected = (
+            [12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 168, 180,
+             192, 204, 216, 228, 240, 252, 264, 276],
+            [48, 96, 144, 192, 240],
+            ['04', '08', '12', '16', '20']
+        )
+        self.assertTupleEqual(getDayTicks(), expected)
+
+
+class TestGetMassposValue(TestCase):
+    """Test suite for getMasspossValue"""
+    def test_string_output(self):
+        """
+        Test basic functionality of getMassposValueColors - the range option
+        and color mode are correct, and the output is a string.
+        """
+        expected_input_output_pairs = {
+            ('regular', 'B'): '0.5:C|2.0:G|4.0:Y|7.0:R|7.0:+M',
+            ('regular', 'W'): '0.5:B|2.0:B|4.0:B|7.0:B|7.0:+B',
+            ('trillium', 'B'): '0.5:C|1.8:G|2.4:Y|3.5:R|3.5:+M',
+            ('trillium', 'W'): '0.5:B|1.8:B|2.4:B|3.5:B|3.5:+B',
+        }
+        test_names = (
+            'test_regular_B',
+            'test_regular_W',
+            'test_trillium_B',
+            'test_trillium_W',
+        )
+        idx = 0
+        for input_val in expected_input_output_pairs:
+            with self.subTest(test_names[idx]):
+                self.assertEqual(
+                    getMassposValueColors(input_val[0], '', input_val[1], []),
+                    expected_input_output_pairs[input_val]
+                )
+            idx += 1
+
+    def test_list_output(self):
+        """
+        Test basic functionality of getMassposValueColors - the range option
+        and color mode are correct, and the output is a list.
+        """
+        expected_input_output_pairs = {
+            ('regular', 'B'):
+                [(0.5, 'C'), (2.0, 'G'), (4.0, 'Y'), (7.0, 'R'), (7.0, 'M')],
+            ('regular', 'W'):
+                [(0.5, 'B'), (2.0, 'B'), (4.0, 'B'), (7.0, 'B'), (7.0, 'B')],
+            ('trillium', 'B'):
+                [(0.5, 'C'), (1.8, 'G'), (2.4, 'Y'), (3.5, 'R'), (3.5, 'M')],
+            ('trillium', 'W'):
+                [(0.5, 'B'), (1.8, 'B'), (2.4, 'B'), (3.5, 'B'), (3.5, 'B')],
+        }
+        test_names = (
+            'test_regular_B',
+            'test_regular_W',
+            'test_trillium_B',
+            'test_trillium_W',
+        )
+        for i, input_val in enumerate(expected_input_output_pairs):
+            with self.subTest(test_names[i]):
+                self.assertListEqual(
+                    getMassposValueColors(
+                        input_val[0], '', input_val[1], [], retType=''),
+                    expected_input_output_pairs[input_val]
+                )
+
+    def test_range_option_not_supported(self):
+        """
+        Test basic functionality of getMassposValueColors - the range option
+        is not supported.
+        """
+        errors = []
+        empty_color_option = ''
+        self.assertIsNone(
+            getMassposValueColors(empty_color_option, '', 'B', errors))
+        self.assertGreater(len(errors), 0)
+
+        errors = []
+        bad_color_option = 'unsupported'
+        self.assertIsNone(
+            getMassposValueColors(bad_color_option, '', 'B', errors))
+        self.assertGreater(len(errors), 0)
+
+    def test_color_mode_not_supported(self):
+        """
+        Test basic functionality of getMassposValueColors - the color mode is
+        not supported.
+        """
+        errors = []
+        empty_color_mode = ''
+        with self.assertRaises(KeyError):
+            getMassposValueColors('regular', '', empty_color_mode, errors)
+
+        errors = []
+        bad_color_mode = 'unsupported'
+        with self.assertRaises(KeyError):
+            getMassposValueColors('regular', '', bad_color_mode, errors)
+
+
+class TestGetTimeTicks(TestCase):
+    """Test suite for getTimeTicks."""
+    def setUp(self) -> None:
+        """Set up text fixtures."""
+        self.label_cnt = 5
+        self.date_fmt = 'YYYYMMDD'
+
+    def test_expected_time_range(self):
+        """
+        Test basic functionality of getTimeTicks - the given time range is
+        expected in the data.
+        """
+
+        with self.subTest('test_less_than_a_minute'):
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 1, 0, 0, 5).timestamp
+            expected = (
+                [1.0, 2.0, 3.0, 4.0],
+                [1.0, 2.0, 3.0, 4.0],
+                ['19700101 00:00:01', '19700101 00:00:02',
+                 '19700101 00:00:03', '19700101 00:00:04']
+            )
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+        with self.subTest('test_at_least_a_minute'):
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 1, 0, 5, 53).timestamp
+            expected = (
+                [60.0, 120.0, 180.0, 240.0, 300.0],
+                [60.0, 120.0, 180.0, 240.0, 300.0],
+                ['19700101 00:01:00', '19700101 00:02:00', '19700101 00:03:00',
+                 '19700101 00:04:00', '19700101 00:05:00']
+            )
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+        with self.subTest('test_at_least_an_hour'):
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 1, 7, 45, 51).timestamp
+            expected = (
+                [3600.0, 7200.0, 10800.0, 14400.0, 18000.0, 21600.0, 25200.0],
+                [3600.0, 10800.0, 18000.0, 25200.0],
+                ['19700101 01:00', '19700101 03:00', '19700101 05:00',
+                 '19700101 07:00']
+            )
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+        with self.subTest('test_at_least_ten_days'):
+            # Exactly 10 days
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 11, 0, 0, 0).timestamp
+            expected = (
+                [86400.0, 172800.0, 259200.0, 345600.0, 432000.0,
+                 518400.0, 604800.0, 691200.0, 777600.0],
+                [86400.0, 259200.0, 432000.0, 604800.0, 777600.0],
+                ['19700102', '19700104', '19700106', '19700108', '19700110']
+            )
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+            # More than 10 days but fewer than 30 days
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 14, 3, 5, 21).timestamp
+            expected = (
+                [86400.0, 172800.0, 259200.0, 345600.0, 432000.0,
+                 518400.0, 604800.0, 691200.0, 777600.0, 864000.0,
+                 950400.0, 1036800.0, 1123200.0],
+                [86400.0, 345600.0, 604800.0, 864000.0, 1123200.0],
+                ['19700102', '19700105', '19700108', '19700111', '19700114']
+            )
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+        with self.subTest('test_at_least_thirty_days'):
+            # Exactly 30 days
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 31, 0, 0, 0).timestamp
+            expected = ([864000.0, 1728000.0],
+                        [864000.0, 1728000.0],
+                        ['19700111', '19700121'])
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+            # More than 30 days
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 3, 21, 21, 43, 53).timestamp
+            expected = (
+                [864000.0, 1728000.0, 2592000.0, 3456000.0, 4320000.0,
+                 5184000.0, 6048000.0],
+                [864000.0, 2592000.0, 4320000.0, 6048000.0],
+                ['19700111', '19700131', '19700220', '19700312']
+            )
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+    def test_boundary_time_range(self):
+        """
+        Test basic functionality of getTimeTicks - the given time range is
+        exactly 1 second, 1 minute, or 1 hour. Test the behavior where these
+        time ranges make getTimeTicks returns a tuple that contains empty
+        lists.
+        """
+        expected = ([], [], [])
+        with self.subTest('test_exactly_one_second'):
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 1, 0, 0, 1).timestamp
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+        with self.subTest('test_exactly_one_minute'):
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 1, 0, 1, 0).timestamp
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+        with self.subTest('test_exactly_one_hour'):
+            earliest = UTCDateTime(1970, 1, 1, 0, 0, 0).timestamp
+            latest = UTCDateTime(1970, 1, 1, 1, 0, 0).timestamp
+            self.assertTupleEqual(
+                getTimeTicks(earliest, latest, self.date_fmt, self.label_cnt),
+                expected
+            )
+
+    def test_earliest_time_later_than_latest_time(self):
+        """
+        Test basic functionality of getTimeTicks - the given latest time is
+        earlier than the earliest time.
+        """
+        self.assertTupleEqual(
+            getTimeTicks(100, 0, self.date_fmt, self.label_cnt),
+            ([], [], [])
+        )
+
+    def test_time_range_is_zero(self):
+        """
+        Test basic functionality of getTimeTicks - the given time range is 0.
+        """
+        self.assertTupleEqual(
+            getTimeTicks(0, 0, self.date_fmt, self.label_cnt),
+            ([], [], [])
+        )
+
+    def test_get_time_ticks_no_label_displayed(self):
+        """
+        Test basic functionality of getTimeTicks - no time label is requested
+        to be displayed.
+        """
+        zero_label_cnt = 0
+        with self.assertRaises(ZeroDivisionError):
+            getTimeTicks(0, 1000, self.date_fmt, zero_label_cnt)
+
+
+class TestFormatTimeAndGetTitle(TestCase):
+    """Test suite for formatTime and getTitle"""
+    def setUp(self) -> None:
+        """Set up text fixtures."""
+        self.positive_epoch_time = 67567567
+        self.positive_formatted_dates = {
+            'YYYY-MM-DD': '1972-02-22',
+            'YYYYMMDD': '19720222',
+            'YYYY:DOY': '1972:053',
+        }
+        self.positive_formatted_time = '00:46:07'
+
+        self.negative_epoch_time = -67567567
+        self.negative_formatted_dates = {
+            'YYYY-MM-DD': '1967-11-10',
+            'YYYYMMDD': '19671110',
+            'YYYY:DOY': '1967:314',
+        }
+        self.negative_formatted_time = '23:13:53'
+
+    def test_format_time_epoch_time_date_only(self):
+        """
+        Test basic functionality of formatTime - given time is epoch time and
+        uses only a date format. Tests three cases for each date format: epoch
+        time is positive, negative, and zero.
+        """
+        # formatter:off
+        test_name_to_date_mode_map = {
+            'test_year_month_day_format':         'YYYY-MM-DD',
+            'test_year_month_day_format_no_dash': 'YYYYMMDD',
+            'test_day_of_year_format':            'YYYY:DOY'
+        }
+        zero_epoch_formatted = {
+            'test_year_month_day_format':         '1970-01-01',
+            'test_year_month_day_format_no_dash': '19700101',
+            'test_day_of_year_format':            '1970:001',
+        }
+        # formatter:on
+        for test_name, date_mode in test_name_to_date_mode_map.items():
+            with self.subTest(test_name):
+                self.assertEqual(
+                    formatTime(self.positive_epoch_time, date_mode),
+                    self.positive_formatted_dates[date_mode])
+                self.assertEqual(
+                    formatTime(self.negative_epoch_time, date_mode),
+                    self.negative_formatted_dates[date_mode])
+                self.assertEqual(
+                    formatTime(0, date_mode),
+                    zero_epoch_formatted[test_name])
+
+    def test_format_time_epoch_time_date_and_time(self):
+        """
+        Test basic functionality of formatTime - given time is epoch time and
+        both a time and a date format are used. Tests three cases for each date
+        format: epoch time is positive, negative, and zero.
+        """
+        # formatter:off
+        test_name_to_date_mode_map = {
+            'test_year_month_day_format':         'YYYY-MM-DD',
+            'test_year_month_day_format_no_dash': 'YYYYMMDD',
+            'test_day_of_year_format':            'YYYY:DOY',
+        }
+        zero_epoch_formatted = {
+            'test_year_month_day_format':         '1970-01-01 00:00:00',
+            'test_year_month_day_format_no_dash': '19700101 00:00:00',
+            'test_day_of_year_format':            '1970:001 00:00:00',
+        }
+        # formatter:on
+        for test_name, date_mode in test_name_to_date_mode_map.items():
+            with self.subTest(test_name):
+                positive_time_expected = (
+                    f'{self.positive_formatted_dates[date_mode]} '
+                    f'{self.positive_formatted_time}'
+                )
+                negative_time_expected = (
+                    f'{self.negative_formatted_dates[date_mode]} '
+                    f'{self.negative_formatted_time}'
+                )
+
+                self.assertEqual(
+                    formatTime(self.positive_epoch_time, date_mode,
+                               'HH:MM:SS'),
+                    positive_time_expected
+                )
+                self.assertEqual(
+                    formatTime(self.negative_epoch_time, date_mode,
+                               'HH:MM:SS'),
+                    negative_time_expected
+                )
+                self.assertEqual(formatTime(0, date_mode, 'HH:MM:SS'),
+                                 zero_epoch_formatted[test_name])
+
+    def test_format_time_UTCDateTime_date_only(self):
+        """
+        Test basic functionality of formatTime - given time is an UTCDateTime
+        instance and uses only a date format.
+        """
+        test_name_to_date_mode_map = {
+            'test_year_month_day_format': 'YYYY-MM-DD',
+            'test_year_month_day_format_no_dash': 'YYYYMMDD',
+            'test_day_of_year_format': 'YYYY:DOY',
+        }
+        utc_date_time = UTCDateTime(self.positive_epoch_time)
+        expected_dates = self.positive_formatted_dates
+        for test_name, date_mode in test_name_to_date_mode_map.items():
+            with self.subTest(test_name):
+                self.assertEqual(formatTime(utc_date_time, date_mode),
+                                 expected_dates[date_mode])
+
+    def test_format_time_UTCDateTime_date_and_time(self):
+        """
+        Test basic functionality of formatTime - given time is an UTCDateTime
+        instance and both a time and a date format are used.
+        """
+        test_name_to_date_mode_map = {
+            'test_year_month_day_format': 'YYYY-MM-DD',
+            'test_year_month_day_format_no_dash': 'YYYYMMDD',
+            'test_day_of_year_format': 'YYYY:DOY',
+        }
+        test_time = UTCDateTime(self.positive_epoch_time)
+        expected_dates = self.positive_formatted_dates
+        expected_time = self.positive_formatted_time
+        for test_name, date_mode in test_name_to_date_mode_map.items():
+            with self.subTest('test_year_month_day_format'):
+                self.assertEqual(
+                    formatTime(test_time, date_mode, 'HH:MM:SS'),
+                    f'{expected_dates[date_mode]} {expected_time}'
+                )
+
+    def test_format_time_unsupported_date_format(self):
+        """
+        Test basic functionality of formatTime - given date format is not
+        supported.
+        """
+        test_time = self.positive_epoch_time
+        empty_format = ''
+        bad_format = 'bad_format'
+
+        with self.subTest('test_without_time_format'):
+            expected = ''
+            self.assertEqual(formatTime(test_time, empty_format),
+                             expected)
+            self.assertEqual(formatTime(test_time, bad_format),
+                             expected)
+
+        with self.subTest('test_with_time_format'):
+            expected = f' {self.positive_formatted_time}'
+            self.assertEqual(formatTime(test_time, empty_format, 'HH:MM:SS'),
+                             expected)
+            self.assertEqual(formatTime(test_time, bad_format, 'HH:MM:SS'),
+                             expected)
+
+    def test_format_time_unsupported_time_format(self):
+        """
+        Test basic functionality of formatTime - given time format is not
+        supported.
+        """
+        test_time = self.positive_epoch_time
+        date_format = 'YYYYMMDD'
+        empty_format = ''
+        bad_format = 'bad_format'
+
+        expected = self.positive_formatted_dates[date_format]
+        self.assertEqual(formatTime(test_time, date_format, empty_format),
+                         expected)
+        self.assertEqual(formatTime(test_time, date_format, bad_format),
+                         expected)
+
+    def test_format_time_unsupported_date_and_time_format(self):
+        """
+        Test basic functionality of formatTime - both given date and time
+        format are unsupported.
+        """
+        test_time = self.positive_epoch_time
+        expected = ''
+        bad_date_formats = ['', 'bad_date_format']
+        bad_time_format = ['', 'bad_time_format']
+        for date_format in bad_date_formats:
+            for time_format in bad_time_format:
+                self.assertEqual(
+                    formatTime(test_time, date_format, time_format),
+                    expected
+                )
+
+    def test_get_title(self):
+        """Test basic functionality of getTitle."""
+        date_mode = 'YYYYMMDD'
+        min_time = 0
+        max_time = self.positive_epoch_time
+        formatted_max_time = (f'{self.positive_formatted_dates[date_mode]}'
+                              f' {self.positive_formatted_time}')
+        with self.subTest('test_mseed'):
+            key = '3734'
+            expected = (f'3734  19700101 00:00:00  to  '
+                        f'{formatted_max_time}  (18768.77)')
+            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
+                             expected)
+        with self.subTest('test_rt130'):
+            key = ('92EB', 25)
+            expected = (f"('92EB', 25)  19700101 00:00:00  to  "
+                        f"{formatted_max_time}  (18768.77)")
+            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
+                             expected)
+
+    def test_get_title_max_time_earlier_than_min_time(self):
+        """
+        Test basic functionality of getTitle - the given maximum time is
+        chronologically earlier than the given minimum time.
+        """
+        date_mode = 'YYYYMMDD'
+        min_time = self.positive_epoch_time
+        max_time = 0
+        formatted_max_time = (f'{self.positive_formatted_dates[date_mode]}'
+                              f' {self.positive_formatted_time}')
+        with self.subTest('test_mseed'):
+            key = '3734'
+            expected = (f'3734  {formatted_max_time}  to  '
+                        f'19700101 00:00:00  (-18768.77)')
+            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
+                             expected)
+        with self.subTest('test_rt130'):
+            key = ('92EB', 25)
+            expected = (f"('92EB', 25)  {formatted_max_time}  to  "
+                        f"19700101 00:00:00  (-18768.77)")
+            self.assertEqual(getTitle(key, min_time, max_time, date_mode),
+                             expected)
+
+
+class TestGetUnitBitweight(TestCase):
+    """Test suite for getUnitBitweight."""
+
+    def setUp(self) -> None:
+        """Set up test fixtures."""
+        self.chan_info = {
+            'channel': 'Test Channel Name',
+            'fixPoint': 0,
+            'plotType': 'test_plot_type',
+            'unit': 'test_unit'
+        }
+        # In most cases, we do not care about the value of bitweightOpt. So, we
+        # give it a default value unless needed.
+        self.default_bitweight_opt = 'low'
+
+    def test_soh_channel_linesDots_linesSRate_linesMasspos_plot_type(self):
+        """
+        Test basic functionality of getUnitBitweight - the given plot type is
+        linesDots, linesSRate, or linesMassposs.
+        """
+        self.chan_info['plotType'] = 'linesDots'
+        with self.subTest('test_no_fix_point'):
+            self.assertEqual(
+                getUnitBitweight(self.chan_info, self.default_bitweight_opt),
+                '{}test_unit'
+            )
+        with self.subTest('test_have_fix_point'):
+            self.chan_info['fixPoint'] = 1
+            self.assertEqual(
+                getUnitBitweight(self.chan_info, self.default_bitweight_opt),
+                '{:.1f}test_unit'
+            )
+
+    def test_soh_channel_other_plot_type(self):
+        """
+        Test basic functionality of getUnitBitweight - the given plot type is
+        not linesDots, linesSRate, or linesMassposs and the channel is not a
+        seismic data channel.
+        """
+        self.assertEqual(
+            getUnitBitweight(self.chan_info, self.default_bitweight_opt),
+            ''
+        )
+
+    def test_seismic_channel_have_fix_point(self):
+        """
+        Test basic functionality of getUnitBitweight - the given plot type is
+        not linesDots, linesSRate, or linesMassposs, the channel is a
+        seismic data channel, and there is a fix point.
+        """
+        self.chan_info['channel'] = 'SEISMIC'
+        self.chan_info['fixPoint'] = 1
+        self.assertEqual(
+            getUnitBitweight(self.chan_info, self.default_bitweight_opt),
+            '{:.1f}test_unit'
+        )
+
+    def test_seismic_channel_no_fix_point(self):
+        """
+        Test basic functionality of getUnitBitweight - the given plot type is
+        not linesDots, linesSRate, or linesMassposs, the channel is a
+        seismic data channel, and there is no fix point.
+        """
+        self.chan_info['channel'] = 'SEISMIC'
+        with self.subTest('test_no_bitweight_option'):
+            self.assertEqual(getUnitBitweight(self.chan_info, 'high'), '{}V')
+        with self.subTest('test_have_bitweight_option'):
+            self.assertEqual(getUnitBitweight(self.chan_info, ''),
+                             '{}test_unit')
+
+    def test_no_fix_point(self):
+        """
+        Test basic functionality of getUnitBitweight - the given channel info
+        does not contain a value for fixPoint.
+        """
+        del self.chan_info['fixPoint']
+        # Asserts that an error is not raised. TestCase does not have a method
+        # to check that an exception was not raised so we have to use this
+        # workaround. For more discussion on this topic, see these
+        # stackoverflow questions and the answers
+        # https://stackoverflow.com/questions/6181555/pass-a-python-unittest-if-an-exception-isnt-raised?noredirect=1&lq=1 # noqa
+        # https://stackoverflow.com/questions/4319825/python-unittest-opposite-of-assertraises/4319870#4319870 # noqa:E501
+
+        # Some context for this code: in getUnitBitWeight, if chanDB does not
+        # have 'fix_point' as a key, then the value of fix_point defaults to 0.
+        # This is implemented by getting the value of 'fix_point' in chanDB and
+        # catching the resulting KeyError. So, in order to test that
+        # getUnitBitweight handles this case correctly, we assert that no
+        # exception was raised.
+        try:
+            getUnitBitweight(self.chan_info, '')
+        except KeyError:
+            self.fail()
-- 
GitLab