|
| 1 | +import os |
| 2 | +import yaml |
| 3 | +import pytest |
| 4 | +from unittest.mock import patch |
| 5 | +from kernelCI_app.management.commands.helpers.file_utils import ( |
| 6 | + load_tree_names, |
| 7 | + move_file_to_failed_dir, |
| 8 | + verify_dir, |
| 9 | + verify_spool_dirs, |
| 10 | +) |
| 11 | +from kernelCI_app.tests.unitTests.helpers.fixtures.file_utils_data import ( |
| 12 | + TREES_PATH_TESTING, |
| 13 | + BASE_TREES_FILE, |
| 14 | + EXPECTED_PARSED_TREES_FILE, |
| 15 | + BASE_FILE_NAME, |
| 16 | + NONEXISTING_FILE_NAME, |
| 17 | + TESTING_FAILED_DIR, |
| 18 | + EXISTING_DIRECTORY, |
| 19 | + MISSING_DIRECTORY, |
| 20 | + DENIED_DIRECTORY, |
| 21 | + NOT_A_DIRECTORY, |
| 22 | + UNACCESSIBLE_DIRECTORY, |
| 23 | + SPOOL_DIR_TESTING, |
| 24 | + FAIL_SPOOL_SUBDIR, |
| 25 | + ARCHIVE_SPOOL_SUBDIR, |
| 26 | +) |
| 27 | + |
| 28 | + |
| 29 | +class TestLoadTreeNames: |
| 30 | + @patch("builtins.open") |
| 31 | + @patch("yaml.safe_load") |
| 32 | + def test_load_tree_names_with_default_file(self, mock_yaml_load, mock_open): |
| 33 | + """Test load_tree_names using default TREES_FILE constant.""" |
| 34 | + mock_yaml_data = BASE_TREES_FILE |
| 35 | + mock_yaml_load.return_value = mock_yaml_data |
| 36 | + |
| 37 | + result = load_tree_names() |
| 38 | + |
| 39 | + mock_open.assert_called_once_with("/app/trees.yaml", "r", encoding="utf-8") |
| 40 | + mock_yaml_load.assert_called_once() |
| 41 | + assert result == EXPECTED_PARSED_TREES_FILE |
| 42 | + |
| 43 | + @patch("builtins.open") |
| 44 | + @patch("yaml.safe_load") |
| 45 | + def test_load_tree_names_with_valid_yaml(self, mock_yaml_load, mock_open): |
| 46 | + """Test load_tree_names with valid YAML data.""" |
| 47 | + mock_yaml_load.return_value = BASE_TREES_FILE |
| 48 | + |
| 49 | + result = load_tree_names(TREES_PATH_TESTING) |
| 50 | + |
| 51 | + mock_open.assert_called_once_with(TREES_PATH_TESTING, "r", encoding="utf-8") |
| 52 | + mock_yaml_load.assert_called_once() |
| 53 | + assert result == EXPECTED_PARSED_TREES_FILE |
| 54 | + |
| 55 | + @patch("builtins.open") |
| 56 | + @patch("yaml.safe_load") |
| 57 | + def test_load_tree_names_with_empty_trees(self, mock_yaml_load, mock_open): |
| 58 | + """Test load_tree_names with empty trees section.""" |
| 59 | + empty_trees_file = {"trees": {}} |
| 60 | + mock_yaml_load.return_value = empty_trees_file |
| 61 | + |
| 62 | + result = load_tree_names(TREES_PATH_TESTING) |
| 63 | + |
| 64 | + mock_open.assert_called_once_with(TREES_PATH_TESTING, "r", encoding="utf-8") |
| 65 | + mock_yaml_load.assert_called_once() |
| 66 | + assert result == {} |
| 67 | + |
| 68 | + @patch("builtins.open") |
| 69 | + @patch("yaml.safe_load") |
| 70 | + def test_load_tree_names_with_no_trees_key(self, mock_yaml_load, mock_open): |
| 71 | + """Test load_tree_names with YAML data missing trees key.""" |
| 72 | + wrong_trees_file = {"not_trees": "any_value"} |
| 73 | + mock_yaml_load.return_value = wrong_trees_file |
| 74 | + |
| 75 | + result = load_tree_names(TREES_PATH_TESTING) |
| 76 | + |
| 77 | + mock_open.assert_called_once_with(TREES_PATH_TESTING, "r", encoding="utf-8") |
| 78 | + mock_yaml_load.assert_called_once() |
| 79 | + assert result == {} |
| 80 | + |
| 81 | + @patch("builtins.open") |
| 82 | + def test_load_tree_names_file_not_found(self, mock_open): |
| 83 | + """Test load_tree_names when file doesn't exist.""" |
| 84 | + mock_open.side_effect = FileNotFoundError("File not found") |
| 85 | + with pytest.raises(FileNotFoundError): |
| 86 | + load_tree_names("/nonexistent/trees.yaml") |
| 87 | + |
| 88 | + @patch("builtins.open") |
| 89 | + @patch("yaml.safe_load") |
| 90 | + def test_load_tree_names_invalid_yaml(self, mock_yaml_load, _): |
| 91 | + """Test load_tree_names with invalid YAML content.""" |
| 92 | + mock_yaml_load.side_effect = yaml.YAMLError("Invalid YAML") |
| 93 | + with pytest.raises(yaml.YAMLError): |
| 94 | + load_tree_names(TREES_PATH_TESTING) |
| 95 | + |
| 96 | + |
| 97 | +class TestMoveFileToFailedDir: |
| 98 | + @patch("os.rename") |
| 99 | + @patch("os.path.basename") |
| 100 | + @patch("os.path.join") |
| 101 | + def test_move_file_to_failed_dir_success( |
| 102 | + self, mock_join, mock_basename, mock_rename |
| 103 | + ): |
| 104 | + """Test successful file move to failed directory.""" |
| 105 | + mock_basename.return_value = BASE_FILE_NAME |
| 106 | + mock_join.return_value = f"{TESTING_FAILED_DIR}/{BASE_FILE_NAME}" |
| 107 | + |
| 108 | + move_file_to_failed_dir(BASE_FILE_NAME, TESTING_FAILED_DIR) |
| 109 | + |
| 110 | + mock_basename.assert_called_once_with(BASE_FILE_NAME) |
| 111 | + mock_join.assert_called_once_with(TESTING_FAILED_DIR, BASE_FILE_NAME) |
| 112 | + mock_rename.assert_called_once_with(BASE_FILE_NAME, mock_join.return_value) |
| 113 | + |
| 114 | + @patch("os.rename") |
| 115 | + @patch("os.path.basename") |
| 116 | + @patch("os.path.join") |
| 117 | + @patch("kernelCI_app.management.commands.helpers.file_utils.logger") |
| 118 | + def test_move_file_to_failed_dir_file_not_found( |
| 119 | + self, mock_logger, mock_join, mock_basename, mock_rename |
| 120 | + ): |
| 121 | + """Test file move when source file doesn't exist.""" |
| 122 | + mock_rename.side_effect = FileNotFoundError("File not found") |
| 123 | + mock_rename.return_value = NONEXISTING_FILE_NAME |
| 124 | + mock_join.return_value = f"{TESTING_FAILED_DIR}/{BASE_FILE_NAME}" |
| 125 | + |
| 126 | + with pytest.raises(FileNotFoundError): |
| 127 | + move_file_to_failed_dir(NONEXISTING_FILE_NAME, TESTING_FAILED_DIR) |
| 128 | + |
| 129 | + mock_logger.error.assert_called_once_with( |
| 130 | + "Error moving file %s to failed directory: %s", |
| 131 | + mock_rename.return_value, |
| 132 | + mock_rename.side_effect, |
| 133 | + ) |
| 134 | + |
| 135 | + |
| 136 | +class TestVerifyDir: |
| 137 | + @patch("os.path.exists") |
| 138 | + @patch("os.path.isdir") |
| 139 | + @patch("os.access") |
| 140 | + @patch("kernelCI_app.management.commands.helpers.file_utils.logger") |
| 141 | + def test_verify_dir_valid_directory( |
| 142 | + self, mock_logger, mock_access, mock_isdir, mock_exists |
| 143 | + ): |
| 144 | + """Test verify_dir with valid, writable directory.""" |
| 145 | + mock_access.return_value = True |
| 146 | + mock_isdir.return_value = True |
| 147 | + mock_exists.return_value = True |
| 148 | + |
| 149 | + verify_dir(EXISTING_DIRECTORY) |
| 150 | + |
| 151 | + mock_exists.assert_called_once_with(EXISTING_DIRECTORY) |
| 152 | + mock_isdir.assert_called_once_with(EXISTING_DIRECTORY) |
| 153 | + mock_access.assert_called_once_with(EXISTING_DIRECTORY, os.W_OK) |
| 154 | + mock_logger.info.assert_called_once_with( |
| 155 | + "Directory %s is valid and writable", EXISTING_DIRECTORY |
| 156 | + ) |
| 157 | + |
| 158 | + @patch("os.path.exists") |
| 159 | + @patch("os.makedirs") |
| 160 | + @patch("os.path.isdir") |
| 161 | + @patch("os.access") |
| 162 | + @patch("kernelCI_app.management.commands.helpers.file_utils.logger") |
| 163 | + def test_verify_dir_creates_missing_directory( |
| 164 | + self, mock_logger, mock_access, mock_isdir, mock_makedirs, mock_exists |
| 165 | + ): |
| 166 | + """Test verify_dir creates valid but missing directory.""" |
| 167 | + mock_exists.return_value = False |
| 168 | + mock_access.return_value = True |
| 169 | + mock_isdir.return_value = True |
| 170 | + |
| 171 | + verify_dir(MISSING_DIRECTORY) |
| 172 | + |
| 173 | + mock_exists.assert_called_once_with(MISSING_DIRECTORY) |
| 174 | + mock_makedirs.assert_called_once_with(MISSING_DIRECTORY) |
| 175 | + mock_logger.info.assert_any_call("Directory %s created", MISSING_DIRECTORY) |
| 176 | + mock_logger.info.assert_any_call( |
| 177 | + "Directory %s is valid and writable", MISSING_DIRECTORY |
| 178 | + ) |
| 179 | + |
| 180 | + @patch("os.path.exists") |
| 181 | + @patch("os.makedirs") |
| 182 | + @patch("kernelCI_app.management.commands.helpers.file_utils.logger") |
| 183 | + def test_verify_dir_fails_to_create_directory( |
| 184 | + self, mock_logger, mock_makedirs, mock_exists |
| 185 | + ): |
| 186 | + """Test verify_dir when directory creation fails.""" |
| 187 | + mock_makedirs.side_effect = PermissionError("Permission denied") |
| 188 | + mock_exists.return_value = False |
| 189 | + |
| 190 | + with pytest.raises(PermissionError): |
| 191 | + verify_dir(DENIED_DIRECTORY) |
| 192 | + |
| 193 | + mock_exists.assert_called_once_with(DENIED_DIRECTORY) |
| 194 | + mock_makedirs.assert_called_once_with(DENIED_DIRECTORY) |
| 195 | + assert mock_logger.error.call_count == 2 |
| 196 | + mock_logger.error.assert_any_call( |
| 197 | + "Directory %s does not exist", DENIED_DIRECTORY |
| 198 | + ) |
| 199 | + mock_logger.error.assert_any_call( |
| 200 | + "Error creating directory %s: %s", |
| 201 | + DENIED_DIRECTORY, |
| 202 | + mock_makedirs.side_effect, |
| 203 | + ) |
| 204 | + |
| 205 | + @patch("os.path.exists") |
| 206 | + @patch("os.path.isdir") |
| 207 | + def test_verify_dir_path_is_not_directory(self, mock_isdir, mock_exists): |
| 208 | + """Test verify_dir when path exists but is not a directory.""" |
| 209 | + mock_isdir.return_value = False |
| 210 | + mock_exists.return_value = True |
| 211 | + |
| 212 | + with pytest.raises( |
| 213 | + Exception, match=f"Directory {NOT_A_DIRECTORY} is not a directory" |
| 214 | + ): |
| 215 | + verify_dir(NOT_A_DIRECTORY) |
| 216 | + |
| 217 | + mock_exists.assert_called_once_with(NOT_A_DIRECTORY) |
| 218 | + mock_isdir.assert_called_once_with(NOT_A_DIRECTORY) |
| 219 | + |
| 220 | + @patch("os.path.exists") |
| 221 | + @patch("os.path.isdir") |
| 222 | + @patch("os.access") |
| 223 | + def test_verify_dir_not_writable(self, mock_access, mock_isdir, mock_exists): |
| 224 | + """Test verify_dir when directory is not writable.""" |
| 225 | + mock_access.return_value = False |
| 226 | + mock_isdir.return_value = True |
| 227 | + mock_exists.return_value = True |
| 228 | + |
| 229 | + with pytest.raises( |
| 230 | + Exception, match=f"Directory {UNACCESSIBLE_DIRECTORY} is not writable" |
| 231 | + ): |
| 232 | + verify_dir(UNACCESSIBLE_DIRECTORY) |
| 233 | + |
| 234 | + mock_exists.assert_called_once_with(UNACCESSIBLE_DIRECTORY) |
| 235 | + mock_isdir.assert_called_once_with(UNACCESSIBLE_DIRECTORY) |
| 236 | + mock_access.assert_called_once_with(UNACCESSIBLE_DIRECTORY, os.W_OK) |
| 237 | + |
| 238 | + |
| 239 | +class TestVerifySpoolDirs: |
| 240 | + @patch("kernelCI_app.management.commands.helpers.file_utils.verify_dir") |
| 241 | + @patch("os.path.join") |
| 242 | + def test_verify_spool_dirs_success(self, mock_join, mock_verify_dir): |
| 243 | + """Test verify_spool_dirs with successful directory verification.""" |
| 244 | + joined_fail_dir = "/".join([SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR]) |
| 245 | + joined_archive_dir = "/".join([SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR]) |
| 246 | + mock_join.side_effect = [joined_fail_dir, joined_archive_dir] |
| 247 | + |
| 248 | + verify_spool_dirs(SPOOL_DIR_TESTING) |
| 249 | + |
| 250 | + assert mock_join.call_count == 2 |
| 251 | + mock_join.assert_any_call(SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR) |
| 252 | + mock_join.assert_any_call(SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR) |
| 253 | + |
| 254 | + assert mock_verify_dir.call_count == 3 |
| 255 | + mock_verify_dir.assert_any_call(SPOOL_DIR_TESTING) |
| 256 | + mock_verify_dir.assert_any_call(joined_fail_dir) |
| 257 | + mock_verify_dir.assert_any_call(joined_archive_dir) |
| 258 | + |
| 259 | + @patch("kernelCI_app.management.commands.helpers.file_utils.verify_dir") |
| 260 | + @patch("os.path.join") |
| 261 | + def test_verify_spool_dirs_join_failure(self, mock_join, mock_verify_dir): |
| 262 | + """Test verify_spool_dirs when os.path.join fails.""" |
| 263 | + mock_join.side_effect = OSError("Join operation failed") |
| 264 | + |
| 265 | + with pytest.raises(OSError): |
| 266 | + verify_spool_dirs(SPOOL_DIR_TESTING) |
| 267 | + |
| 268 | + mock_join.assert_called_once_with(SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR) |
| 269 | + mock_verify_dir.assert_not_called() |
| 270 | + |
| 271 | + @patch("kernelCI_app.management.commands.helpers.file_utils.verify_dir") |
| 272 | + @patch("os.path.join") |
| 273 | + def test_verify_spool_dirs_verify_spool_dir_fails(self, mock_join, mock_verify_dir): |
| 274 | + """Test verify_spool_dirs when spool directory verification fails.""" |
| 275 | + joined_fail_dir = "/".join([SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR]) |
| 276 | + joined_archive_dir = "/".join([SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR]) |
| 277 | + mock_join.side_effect = [joined_fail_dir, joined_archive_dir] |
| 278 | + |
| 279 | + # Meant to represent any kind of failure in verify_dir |
| 280 | + mock_verify_dir.side_effect = Exception("Spool directory verification failed") |
| 281 | + |
| 282 | + with pytest.raises(Exception, match="Spool directory verification failed"): |
| 283 | + verify_spool_dirs(SPOOL_DIR_TESTING) |
| 284 | + |
| 285 | + assert mock_join.call_count == 2 |
| 286 | + mock_join.assert_any_call(SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR) |
| 287 | + mock_join.assert_any_call(SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR) |
| 288 | + |
| 289 | + mock_verify_dir.assert_called_once_with(SPOOL_DIR_TESTING) |
0 commit comments