diff --git a/tests/test_controller/test_util.py b/tests/test_controller/test_util.py new file mode 100644 index 0000000000000000000000000000000000000000..8c660a5c5cb31e023273c3b3c997c806668583b9 --- /dev/null +++ b/tests/test_controller/test_util.py @@ -0,0 +1,343 @@ +"""Test suite for functions defined in sohstationviewer.controller.util.""" + +import os + +from unittest import TestCase +from unittest.mock import patch +from tempfile import TemporaryDirectory, NamedTemporaryFile +import string + + +from sohstationviewer.controller.util import ( + validateFile, + getDirSize, + getTime6, + getTime6_2y, + getTime6_4y, + getTime4, + getVal, + rtnPattern, + fmti +) + +TEST_DATA_DIR = os.path.realpath(os.path.join( + os.path.dirname(os.path.realpath(__file__)), + os.pardir, + 'test_data', +)) + + +class TestGetTime(TestCase): + """Test suite for getTime6, getTime6_2y, getTime6_4y, and getTime4.""" + def setUp(self): + """Set up text fixtures.""" + self.time6_2y = '01:251:09:41:35:656' + self.time6_4y = '2001:251:09:41:35:656' + self.time4_day_1 = '1:09:41:35' + self.time4 = '251:09:41:35' + + @patch('sohstationviewer.controller.util.getTime6_4y') + @patch('sohstationviewer.controller.util.getTime6_2y') + def test_get_time6(self, mock_2y, mock_4y): + """ + Test getTime6 - check that getTime6 delegates work to the appropriate + helper function depending on the input. + """ + with self.subTest('test_2_digit_year'): + getTime6(self.time6_2y) + self.assertTrue(mock_2y.called) + self.assertFalse(mock_4y.called) + mock_2y.reset_mock() + mock_4y.reset_mock() + with self.subTest('test_4_digit_year'): + getTime6(self.time6_4y) + self.assertTrue(mock_4y.called) + self.assertFalse(mock_2y.called) + + def test_get_time6_invalid_input(self): + """Test getTime6 - the input is not one of the expected formats.""" + with self.subTest('test_input_contains_colon'): + bad_inputs = [':523:531:', 'fs:523:531:', 'towe:523:531:'] + for input_str in bad_inputs: + with self.assertRaises(ValueError): + getTime6(input_str) + with self.subTest('test_input_does_not_contain_colon'): + input_str = 'fq31dqrt63' + with self.assertRaises(ValueError): + getTime6(input_str) + + def test_get_time6_2y(self): + """Test getTime6_2y.""" + epoch_time, year = getTime6_2y(self.time6_2y) + self.assertAlmostEqual(epoch_time, 999942095.656) + self.assertEqual(year, 2001) + + def test_get_time6_4y(self): + """Test getTime6_4y.""" + epoch_time, year = getTime6_4y(self.time6_4y) + self.assertAlmostEqual(epoch_time, 999942095.656) + self.assertEqual(year, 2001) + + def test_get_time4_year_added(self): + """Test getTime4 - a year has been added.""" + year = 2001 + year_added = True + with self.subTest('test_first_day_of_year'): + epoch_time, ret_year, ret_year_added = ( + getTime4(self.time4_day_1, year, year_added) + ) + self.assertEqual(epoch_time, 978342095) + self.assertEqual(ret_year, year) + self.assertTrue(ret_year_added) + + with self.subTest('test_other_days'): + epoch_time, ret_year, ret_year_added = ( + getTime4(self.time4, year, year_added) + ) + self.assertEqual(epoch_time, 999942095) + self.assertEqual(ret_year, year) + self.assertTrue(ret_year_added) + + def test_get_time4_year_not_added(self): + """Test getTime4 - a year has not been added.""" + year = 2001 + year_added = False + with self.subTest('test_first_day_of_year'): + epoch_time, ret_year, ret_year_added = ( + getTime4(self.time4_day_1, year, year_added) + ) + self.assertEqual(epoch_time, 1009878095) + self.assertEqual(ret_year, year + 1) + self.assertTrue(ret_year_added) + + with self.subTest('test_other_days'): + epoch_time, ret_year, ret_year_added = ( + getTime4(self.time4, year, year_added) + ) + self.assertEqual(epoch_time, 999942095) + self.assertEqual(ret_year, year) + self.assertFalse(ret_year_added) + + +class TestValidateFile(TestCase): + """Test suite for validateFile.""" + def test_valid_file(self): + """ + Test basic functionality of validateFile - given file exists and is not + an info file. + """ + with NamedTemporaryFile() as valid_file: + self.assertTrue( + validateFile(valid_file.name, + os.path.basename(valid_file.name)) + ) + + def test_info_file(self): + """ + Test basic functionality of validateFile - given file exists and is an + info file. + """ + with self.subTest('test_dot_DS_Store'): + with TemporaryDirectory() as temp_dir: + # There are two ways to create a temporary file with a + # custom name. The first way is to create a temporary + # directory, then create a file in that directory. When the + # temporary directory is deleted, the file is also deleted. + # This automatic deletion effectively makes the file we created + # a temporary file. + # The second way is to create a temporary file and rename it + # to the name we want. Then, we need to rename the file back + # to its original name once we are done. This renaming is + # needed because temporary file deletes itself based on its + # name attribute, which is not changed in the first + # renaming. While the second way makes it explicit that we + # only want a temporary file, it might not be clear why the + # second renaming is needed. In order to cause less + # confusion in the code, we create this temporary file the + # first way. + ds_store_path = os.path.join(temp_dir, '.DS_Store') + with open(ds_store_path, 'w+') as ds_store_file: + self.assertFalse( + validateFile(ds_store_path, + os.path.basename(ds_store_file.name)) + ) + + with self.subTest('test_dot_underscore'): + with NamedTemporaryFile(prefix='._') as info_file: + self.assertFalse( + validateFile(info_file.name, + os.path.basename(info_file.name)) + ) + + def test_file_does_not_exist(self): + """ + Test basic functionality of validateFile - given file does not exist. + """ + empty_name_file = '' + self.assertFalse(validateFile(empty_name_file, empty_name_file)) + + not_exist_file = 'file_does_not_exist' + self.assertFalse(validateFile(not_exist_file, not_exist_file)) + + +class TestGetDirSize(TestCase): + """Test suite for getDirSize.""" + def test_files_have_size_zero(self): + """Test getDirSize - all files in the given directory has size zero.""" + expected_file_count = 10 + with TemporaryDirectory() as directory: + files = [] + for i in range(expected_file_count): + files.append(NamedTemporaryFile(dir=directory)) + dir_size, dir_file_count = getDirSize(directory) + self.assertEqual(dir_size, 0) + self.assertEqual(dir_file_count, expected_file_count) + # Explicitly clean up the temporary files. If we don't do this, + # the temporary directory will clean up itself and delete the + # temporary files. Then, when the function returns, the references + # to these temporary files will attempt to clean up the files. This + # leads to exceptions being raised because the files being cleaned + # up does not exist any more. + files = [file.close() for file in files] + + def test_files_have_size_greater_than_zero(self): + """ + Test getDirSize - all files in the given directory have a size greater + than zero. + """ + expected_file_count = 10 + size_one_file = 10 + # Each byte character is 1 byte, so we can obtain a byte string of n + # bytes by creating a byte string with n characters. + byte_to_write = b'a' * size_one_file + temp_dir = TemporaryDirectory() + with TemporaryDirectory(dir=temp_dir.name) as directory: + files = [] + for i in range(expected_file_count): + temp_file = NamedTemporaryFile(dir=directory) + temp_file.write(byte_to_write) + # Writing to file is not done until requested so we need to + # explicitly tell Python to finish the write. Otherwise, the + # size of the file on disk will stay 0. + temp_file.flush() + files.append(temp_file) + dir_size, dir_file_count = getDirSize(temp_dir.name) + self.assertEqual(dir_size, size_one_file * expected_file_count) + self.assertEqual(dir_file_count, expected_file_count) + # Explicitly clean up the temporary files. If we don't do this, + # the temporary directory will clean up itself and delete the + # temporary files. Then, when the function returns, the references + # to these temporary files will attempt to clean up the files. This + # leads to exceptions being raised because the files being cleaned + # up does not exist any more. + files = [file.close() for file in files] + + def test_nested_folder_structure(self): + """ + Test getDirSize - the given directory contains nested directories. + """ + test_folder = os.path.join(TEST_DATA_DIR, 'Pegasus-sample') + dir_size, dir_file_count = getDirSize(test_folder) + self.assertEqual(dir_size, 7909115) + self.assertEqual(dir_file_count, 8) + + def test_empty_directory(self): + """Test getDirSize - the given directory contains no file.""" + with TemporaryDirectory() as temp_dir: + dir_size, dir_file_count = getDirSize(temp_dir) + self.assertEqual(dir_size, 0) + self.assertEqual(dir_file_count, 0) + + def test_directory_does_not_exist(self): + """Test getDirSize - the given directory does not exist.""" + empty_name_dir = '' + dir_size, dir_file_count = getDirSize(empty_name_dir) + self.assertEqual(dir_size, 0) + self.assertEqual(dir_file_count, 0) + + non_existent_dir = 'directory does not exist' + dir_size, dir_file_count = getDirSize(non_existent_dir) + self.assertEqual(dir_size, 0) + self.assertEqual(dir_file_count, 0) + + +class TestRtnPattern(TestCase): + """Test suite for rtnPattern.""" + def test_no_upper(self): + """Test rtnPattern - characters are not converted to uppercase.""" + with self.subTest('test_digit'): + digits = '123456789' + self.assertEqual(rtnPattern(digits), '0' * len(digits)) + with self.subTest('test_lowercase'): + lowercase_chars = string.ascii_lowercase + self.assertEqual(rtnPattern(lowercase_chars), + 'a' * len(lowercase_chars)) + with self.subTest('test_uppercase'): + uppercase_chars = string.ascii_uppercase + self.assertEqual(rtnPattern(uppercase_chars), + 'A' * len(uppercase_chars)) + + def test_with_upper(self): + """Test rtnPattern - all characters are converted to uppercase.""" + lowercase_chars = string.ascii_lowercase + self.assertEqual(rtnPattern(lowercase_chars, upper=True), + 'A' * len(lowercase_chars)) + + +class TestGetVal(TestCase): + """Test suite for getVal.""" + def test_normal_case(self): + """Test getVal - the input is of an expected value.""" + # formatter:off + test_name_to_test_map = { + 'test_with_decimal_point': ('60.3V', 60.3), + 'test_no_decimal_point': ('15V', 15.0), + 'test_with_plus_sign': ('+35.5V', 35.5), + 'test_with_negative_sign': ('-35.2V', -35.2), + 'test_multiple_decimal_digits': ('52.523V', 52.5), + 'test_no_decimal_digit': ('12.V', 12.0), + } + # formatter:on + for test_name, inout_pair in test_name_to_test_map.items(): + with self.subTest(test_name): + self.assertEqual(getVal(inout_pair[0]), inout_pair[1]) + + def test_positive_negative_sign_in_front(self): + """ + Test rtnPattern - the input has both a positive sign and a negative + sign in the front. + """ + with self.assertRaises(ValueError): + getVal('+-1.0V') + + def test_bad_input(self): + """Test rtnPattern - the input has a value that is not expected.""" + with self.assertRaises(AttributeError): + getVal('') + + +class TestFmti(TestCase): + """Test suite for fmti.""" + def test_absolute_value_below_1000(self): + """ + Test fmti - the input is greater than -1000 but smaller than 1000. + """ + with self.subTest('test_positive'): + val = 52.521 + self.assertEqual(fmti(val), '52') + with self.subTest('test_negative'): + val = -232.42 + self.assertEqual(fmti(val), '-232') + + def test_absolute_value_above_1000(self): + """ + Test fmti - the input is greater than 1000 or smaller than -1000. + """ + with self.subTest('test_positive'): + val = 136235646.215151 + expected = '136,235,646' + self.assertEqual(fmti(val), expected) + with self.subTest('test_negative'): + val = -62362.32523 + expected = '-62,362' + self.assertEqual(fmti(val), expected)