-
Notifications
You must be signed in to change notification settings - Fork 19
Test: ingester file functions unit tests #1592
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
MarceloRobert
merged 1 commit into
kernelci:main
from
MarceloRobert:feat/ingester-unit-one
Nov 6, 2025
+323
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
289 changes: 289 additions & 0 deletions
289
backend/kernelCI_app/tests/unitTests/commands/monitorSubmissions/file_utils_test.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| import os | ||
| import yaml | ||
| import pytest | ||
| from unittest.mock import patch | ||
| from kernelCI_app.management.commands.helpers.file_utils import ( | ||
| load_tree_names, | ||
| move_file_to_failed_dir, | ||
| verify_dir, | ||
| verify_spool_dirs, | ||
| ) | ||
| from kernelCI_app.tests.unitTests.helpers.fixtures.file_utils_data import ( | ||
| TREES_PATH_TESTING, | ||
| BASE_TREES_FILE, | ||
| EXPECTED_PARSED_TREES_FILE, | ||
| BASE_FILE_NAME, | ||
| NONEXISTING_FILE_NAME, | ||
| TESTING_FAILED_DIR, | ||
| EXISTING_DIRECTORY, | ||
| MISSING_DIRECTORY, | ||
| DENIED_DIRECTORY, | ||
| NOT_A_DIRECTORY, | ||
| INACCESSIBLE_DIRECTORY, | ||
| SPOOL_DIR_TESTING, | ||
| FAIL_SPOOL_SUBDIR, | ||
| ARCHIVE_SPOOL_SUBDIR, | ||
| ) | ||
|
|
||
|
|
||
| class TestLoadTreeNames: | ||
| @patch("builtins.open") | ||
| @patch("yaml.safe_load") | ||
| def test_load_tree_names_with_default_file(self, mock_yaml_load, mock_open): | ||
| """Test load_tree_names using default TREES_FILE constant.""" | ||
| mock_yaml_data = BASE_TREES_FILE | ||
| mock_yaml_load.return_value = mock_yaml_data | ||
|
|
||
| result = load_tree_names() | ||
|
|
||
| mock_open.assert_called_once_with("/app/trees.yaml", "r", encoding="utf-8") | ||
| mock_yaml_load.assert_called_once() | ||
| assert result == EXPECTED_PARSED_TREES_FILE | ||
|
|
||
| @patch("builtins.open") | ||
| @patch("yaml.safe_load") | ||
| def test_load_tree_names_with_valid_yaml(self, mock_yaml_load, mock_open): | ||
| """Test load_tree_names with valid YAML data.""" | ||
| mock_yaml_load.return_value = BASE_TREES_FILE | ||
|
|
||
| result = load_tree_names(TREES_PATH_TESTING) | ||
|
|
||
| mock_open.assert_called_once_with(TREES_PATH_TESTING, "r", encoding="utf-8") | ||
| mock_yaml_load.assert_called_once() | ||
| assert result == EXPECTED_PARSED_TREES_FILE | ||
|
|
||
| @patch("builtins.open") | ||
| @patch("yaml.safe_load") | ||
| def test_load_tree_names_with_empty_trees(self, mock_yaml_load, mock_open): | ||
| """Test load_tree_names with empty trees section.""" | ||
| empty_trees_file = {"trees": {}} | ||
| mock_yaml_load.return_value = empty_trees_file | ||
|
|
||
| result = load_tree_names(TREES_PATH_TESTING) | ||
|
|
||
| mock_open.assert_called_once_with(TREES_PATH_TESTING, "r", encoding="utf-8") | ||
| mock_yaml_load.assert_called_once() | ||
| assert result == {} | ||
|
|
||
| @patch("builtins.open") | ||
| @patch("yaml.safe_load") | ||
| def test_load_tree_names_with_no_trees_key(self, mock_yaml_load, mock_open): | ||
| """Test load_tree_names with YAML data missing trees key.""" | ||
| wrong_trees_file = {"not_trees": "any_value"} | ||
| mock_yaml_load.return_value = wrong_trees_file | ||
|
|
||
| result = load_tree_names(TREES_PATH_TESTING) | ||
|
|
||
| mock_open.assert_called_once_with(TREES_PATH_TESTING, "r", encoding="utf-8") | ||
| mock_yaml_load.assert_called_once() | ||
| assert result == {} | ||
|
|
||
| @patch("builtins.open") | ||
| def test_load_tree_names_file_not_found(self, mock_open): | ||
| """Test load_tree_names when file doesn't exist.""" | ||
| mock_open.side_effect = FileNotFoundError("File not found") | ||
| with pytest.raises(FileNotFoundError): | ||
| load_tree_names("/nonexistent/trees.yaml") | ||
|
|
||
| @patch("builtins.open") | ||
| @patch("yaml.safe_load") | ||
| def test_load_tree_names_invalid_yaml(self, mock_yaml_load, _): | ||
| """Test load_tree_names with invalid YAML content.""" | ||
| mock_yaml_load.side_effect = yaml.YAMLError("Invalid YAML") | ||
| with pytest.raises(yaml.YAMLError): | ||
| load_tree_names(TREES_PATH_TESTING) | ||
|
|
||
|
|
||
| class TestMoveFileToFailedDir: | ||
| @patch("os.rename") | ||
| @patch("os.path.basename") | ||
| @patch("os.path.join") | ||
| def test_move_file_to_failed_dir_success( | ||
| self, mock_join, mock_basename, mock_rename | ||
| ): | ||
| """Test successful file move to failed directory.""" | ||
| mock_basename.return_value = BASE_FILE_NAME | ||
| mock_join.return_value = f"{TESTING_FAILED_DIR}/{BASE_FILE_NAME}" | ||
|
|
||
| move_file_to_failed_dir(BASE_FILE_NAME, TESTING_FAILED_DIR) | ||
|
|
||
| mock_basename.assert_called_once_with(BASE_FILE_NAME) | ||
| mock_join.assert_called_once_with(TESTING_FAILED_DIR, BASE_FILE_NAME) | ||
| mock_rename.assert_called_once_with(BASE_FILE_NAME, mock_join.return_value) | ||
|
|
||
| @patch("os.rename") | ||
| @patch("os.path.basename") | ||
| @patch("os.path.join") | ||
| @patch("kernelCI_app.management.commands.helpers.file_utils.logger") | ||
| def test_move_file_to_failed_dir_file_not_found( | ||
| self, mock_logger, mock_join, mock_basename, mock_rename | ||
| ): | ||
| """Test file move when source file doesn't exist.""" | ||
| mock_rename.side_effect = FileNotFoundError("File not found") | ||
| mock_join.return_value = f"{TESTING_FAILED_DIR}/{BASE_FILE_NAME}" | ||
|
|
||
| with pytest.raises(FileNotFoundError): | ||
| move_file_to_failed_dir(NONEXISTING_FILE_NAME, TESTING_FAILED_DIR) | ||
|
|
||
| mock_logger.error.assert_called_once_with( | ||
| "Error moving file %s to failed directory: %s", | ||
| NONEXISTING_FILE_NAME, | ||
| mock_rename.side_effect, | ||
| ) | ||
| mock_basename.assert_called_once() | ||
|
|
||
|
|
||
| class TestVerifyDir: | ||
| @patch("os.path.exists") | ||
| @patch("os.path.isdir") | ||
| @patch("os.access") | ||
| @patch("kernelCI_app.management.commands.helpers.file_utils.logger") | ||
| def test_verify_dir_valid_directory( | ||
| self, mock_logger, mock_access, mock_isdir, mock_exists | ||
| ): | ||
| """Test verify_dir with valid, writable directory.""" | ||
| mock_access.return_value = True | ||
| mock_isdir.return_value = True | ||
| mock_exists.return_value = True | ||
|
|
||
| verify_dir(EXISTING_DIRECTORY) | ||
|
|
||
| mock_exists.assert_called_once_with(EXISTING_DIRECTORY) | ||
| mock_isdir.assert_called_once_with(EXISTING_DIRECTORY) | ||
| mock_access.assert_called_once_with(EXISTING_DIRECTORY, os.W_OK) | ||
| mock_logger.info.assert_called_once_with( | ||
| "Directory %s is valid and writable", EXISTING_DIRECTORY | ||
| ) | ||
|
|
||
| @patch("os.path.exists") | ||
| @patch("os.makedirs") | ||
| @patch("os.path.isdir") | ||
| @patch("os.access") | ||
| @patch("kernelCI_app.management.commands.helpers.file_utils.logger") | ||
| def test_verify_dir_creates_missing_directory( | ||
| self, mock_logger, mock_access, mock_isdir, mock_makedirs, mock_exists | ||
| ): | ||
| """Test verify_dir creates valid but missing directory.""" | ||
| mock_exists.return_value = False | ||
| mock_access.return_value = True | ||
| mock_isdir.return_value = True | ||
|
|
||
| verify_dir(MISSING_DIRECTORY) | ||
|
|
||
| mock_exists.assert_called_once_with(MISSING_DIRECTORY) | ||
| mock_makedirs.assert_called_once_with(MISSING_DIRECTORY) | ||
| mock_logger.info.assert_any_call("Directory %s created", MISSING_DIRECTORY) | ||
| mock_logger.info.assert_any_call( | ||
| "Directory %s is valid and writable", MISSING_DIRECTORY | ||
| ) | ||
|
|
||
| @patch("os.path.exists") | ||
| @patch("os.makedirs") | ||
| @patch("kernelCI_app.management.commands.helpers.file_utils.logger") | ||
| def test_verify_dir_fails_to_create_directory( | ||
| self, mock_logger, mock_makedirs, mock_exists | ||
| ): | ||
| """Test verify_dir when directory creation fails.""" | ||
| mock_makedirs.side_effect = PermissionError("Permission denied") | ||
| mock_exists.return_value = False | ||
|
|
||
| with pytest.raises(PermissionError): | ||
| verify_dir(DENIED_DIRECTORY) | ||
|
|
||
| mock_exists.assert_called_once_with(DENIED_DIRECTORY) | ||
| mock_makedirs.assert_called_once_with(DENIED_DIRECTORY) | ||
| assert mock_logger.error.call_count == 2 | ||
| mock_logger.error.assert_any_call( | ||
| "Directory %s does not exist", DENIED_DIRECTORY | ||
| ) | ||
| mock_logger.error.assert_any_call( | ||
| "Error creating directory %s: %s", | ||
| DENIED_DIRECTORY, | ||
| mock_makedirs.side_effect, | ||
| ) | ||
|
|
||
| @patch("os.path.exists") | ||
| @patch("os.path.isdir") | ||
| def test_verify_dir_path_is_not_directory(self, mock_isdir, mock_exists): | ||
| """Test verify_dir when path exists but is not a directory.""" | ||
| mock_isdir.return_value = False | ||
| mock_exists.return_value = True | ||
|
|
||
| with pytest.raises( | ||
| Exception, match=f"Directory {NOT_A_DIRECTORY} is not a directory" | ||
| ): | ||
| verify_dir(NOT_A_DIRECTORY) | ||
|
|
||
| mock_exists.assert_called_once_with(NOT_A_DIRECTORY) | ||
| mock_isdir.assert_called_once_with(NOT_A_DIRECTORY) | ||
|
|
||
| @patch("os.path.exists") | ||
| @patch("os.path.isdir") | ||
| @patch("os.access") | ||
| def test_verify_dir_not_writable(self, mock_access, mock_isdir, mock_exists): | ||
| """Test verify_dir when directory is not writable.""" | ||
| mock_access.return_value = False | ||
| mock_isdir.return_value = True | ||
| mock_exists.return_value = True | ||
|
|
||
| with pytest.raises( | ||
| Exception, match=f"Directory {INACCESSIBLE_DIRECTORY} is not writable" | ||
| ): | ||
| verify_dir(INACCESSIBLE_DIRECTORY) | ||
|
|
||
| mock_exists.assert_called_once_with(INACCESSIBLE_DIRECTORY) | ||
| mock_isdir.assert_called_once_with(INACCESSIBLE_DIRECTORY) | ||
| mock_access.assert_called_once_with(INACCESSIBLE_DIRECTORY, os.W_OK) | ||
|
|
||
|
|
||
| class TestVerifySpoolDirs: | ||
| @patch("kernelCI_app.management.commands.helpers.file_utils.verify_dir") | ||
| @patch("os.path.join") | ||
| def test_verify_spool_dirs_success(self, mock_join, mock_verify_dir): | ||
| """Test verify_spool_dirs with successful directory verification.""" | ||
| joined_fail_dir = "/".join([SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR]) | ||
| joined_archive_dir = "/".join([SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR]) | ||
| mock_join.side_effect = [joined_fail_dir, joined_archive_dir] | ||
|
|
||
| verify_spool_dirs(SPOOL_DIR_TESTING) | ||
|
|
||
| assert mock_join.call_count == 2 | ||
| mock_join.assert_any_call(SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR) | ||
| mock_join.assert_any_call(SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR) | ||
|
|
||
| assert mock_verify_dir.call_count == 3 | ||
| mock_verify_dir.assert_any_call(SPOOL_DIR_TESTING) | ||
| mock_verify_dir.assert_any_call(joined_fail_dir) | ||
| mock_verify_dir.assert_any_call(joined_archive_dir) | ||
|
|
||
| @patch("kernelCI_app.management.commands.helpers.file_utils.verify_dir") | ||
| @patch("os.path.join") | ||
| def test_verify_spool_dirs_join_failure(self, mock_join, mock_verify_dir): | ||
| """Test verify_spool_dirs when os.path.join fails.""" | ||
| mock_join.side_effect = TypeError("Really any Exception, this is an example") | ||
|
|
||
| with pytest.raises(TypeError): | ||
| verify_spool_dirs(SPOOL_DIR_TESTING) | ||
|
|
||
| mock_join.assert_called_once_with(SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR) | ||
| mock_verify_dir.assert_not_called() | ||
|
|
||
| @patch("kernelCI_app.management.commands.helpers.file_utils.verify_dir") | ||
| @patch("os.path.join") | ||
| def test_verify_spool_dirs_verify_spool_dir_fails(self, mock_join, mock_verify_dir): | ||
| """Test verify_spool_dirs when spool directory verification fails.""" | ||
| joined_fail_dir = "/".join([SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR]) | ||
| joined_archive_dir = "/".join([SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR]) | ||
| mock_join.side_effect = [joined_fail_dir, joined_archive_dir] | ||
|
|
||
| # Meant to represent any kind of failure in verify_dir | ||
| mock_verify_dir.side_effect = Exception("Spool directory verification failed") | ||
|
|
||
| with pytest.raises(Exception, match="Spool directory verification failed"): | ||
| verify_spool_dirs(SPOOL_DIR_TESTING) | ||
|
|
||
| assert mock_join.call_count == 2 | ||
| mock_join.assert_any_call(SPOOL_DIR_TESTING, FAIL_SPOOL_SUBDIR) | ||
| mock_join.assert_any_call(SPOOL_DIR_TESTING, ARCHIVE_SPOOL_SUBDIR) | ||
|
|
||
| mock_verify_dir.assert_called_once_with(SPOOL_DIR_TESTING) | ||
34 changes: 34 additions & 0 deletions
34
backend/kernelCI_app/tests/unitTests/helpers/fixtures/file_utils_data.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| TREES_PATH_TESTING = "/test/trees.yaml" | ||
|
|
||
| mainline_url = "https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git" | ||
| next_url = "https://git.kernel.org/pub/scm/linux/kernel/git/next/linux-next.git" | ||
| stable_url = "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git" | ||
|
|
||
| BASE_TREES_FILE = { | ||
| "trees": { | ||
| "mainline": {"url": mainline_url}, | ||
| "next": {"url": next_url}, | ||
| "stable": {"url": stable_url}, | ||
| } | ||
| } | ||
|
|
||
| EXPECTED_PARSED_TREES_FILE = { | ||
| mainline_url: "mainline", | ||
| next_url: "next", | ||
| stable_url: "stable", | ||
| } | ||
|
|
||
|
|
||
| BASE_FILE_NAME = "test_file.json" | ||
| NONEXISTING_FILE_NAME = "nonexistent_file.json" | ||
| TESTING_FAILED_DIR = "/failed/dir" | ||
|
|
||
| EXISTING_DIRECTORY = "/existing/directory" | ||
| MISSING_DIRECTORY = "/missing/directory" | ||
| DENIED_DIRECTORY = "/denied/directory" | ||
| NOT_A_DIRECTORY = "/not/a/directory.file" | ||
| INACCESSIBLE_DIRECTORY = "/inaccessible/directory" | ||
|
|
||
| SPOOL_DIR_TESTING = "/spool" | ||
| FAIL_SPOOL_SUBDIR = "failed" | ||
| ARCHIVE_SPOOL_SUBDIR = "archive" |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.