Files
rocm-systems/tests/test_db_connector.py
T
vedithal-amd 80ea339217 Fix test cases (#837)
* Fix formatting
2025-07-28 16:19:45 -04:00

418 righe
15 KiB
Python

##############################################################################bl
# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
##############################################################################el
import logging
import shutil
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, Mock, call, patch
import pandas as pd
import pytest
logging.TRACE = logging.DEBUG - 5
logging.addLevelName(logging.TRACE, "TRACE")
def trace_logger(message, *args, **kwargs):
logging.log(logging.TRACE, message, *args, **kwargs)
setattr(logging, "trace", trace_logger)
from db_connector import DatabaseConnector
"""
Tests for the DatabaseConnector class that tests almost methods with initialization,
CSV import, database removal, and error handling.
The tests use mocks instead of a real MongoDB server for speed and reliability.
"""
class TestDatabaseConnector:
@pytest.fixture
def mock_args_import(self):
"""Mock arguments for import operation"""
args = Mock()
args.username = "test_user"
args.password = "test_pass"
args.host = "localhost"
args.port = 27017
args.team = "test_team"
args.workload = "/app/tests/workloads/device_filter/MI100"
args.upload = True
args.remove = False
args.kernel_verbose = False
return args
@pytest.fixture
def mock_args_remove(self):
"""Mock arguments for remove operation"""
args = Mock()
args.username = "test_user"
args.password = "test_pass"
args.host = "localhost"
args.port = 27017
args.team = "test_team"
args.workload = "rocprofiler-compute_test_team_workload_mi100"
args.upload = False
args.remove = True
args.kernel_verbose = False
return args
def test_init(self, mock_args_import):
"""Test DatabaseConnector initialization"""
connector = DatabaseConnector(mock_args_import)
assert connector.args == mock_args_import
assert isinstance(connector.cache, dict)
assert len(connector.cache) == 0
expected_connection_info = {
"username": "test_user",
"password": "test_pass",
"host": "localhost",
"port": "27017",
"team": "test_team",
"workload": "/app/tests/workloads/device_filter/MI100",
"db": None,
}
assert connector.connection_info == expected_connection_info
assert connector.interaction_type is None
assert connector.client is None
@patch("db_connector.pd.read_csv")
@patch("db_connector.Path")
def test_prep_import_success(self, mock_path, mock_read_csv, mock_args_import):
"""Test successful prep_import"""
# Setup mocks
mock_path.return_value.joinpath.return_value = "/fake/path/sysinfo.csv"
mock_path.return_value.is_file.return_value = True
mock_sysinfo = pd.DataFrame(
{"gpu_model": ["MI100 "], "workload_name": [" test_workload"]}
)
mock_read_csv.return_value = mock_sysinfo
connector = DatabaseConnector(mock_args_import)
connector.prep_import()
expected_db = "rocprofiler-compute_test_team_test_workload_MI100"
assert connector.connection_info["db"] == expected_db
@patch("db_connector.pd.read_csv")
@patch("db_connector.Path")
def test_prep_import_missing_file(self, mock_path, mock_read_csv, mock_args_import):
"""Test prep_import when sysinfo.csv is missing"""
mock_path.return_value.joinpath.return_value = "/fake/path/sysinfo.csv"
mock_path.return_value.is_file.return_value = False
connector = DatabaseConnector(mock_args_import)
with patch(
"db_connector.console_error", side_effect=SystemExit(1)
) as mock_console_error:
with pytest.raises(SystemExit):
connector.prep_import()
mock_console_error.assert_called_with(
"database", "Unable to parse SoC and/or workload name from sysinfo.csv"
)
@patch("db_connector.pd.read_csv")
@patch("db_connector.Path")
def test_prep_import_key_error(self, mock_path, mock_read_csv, mock_args_import):
"""Test prep_import when required fields are missing"""
mock_path.return_value.joinpath.return_value = "/fake/path/sysinfo.csv"
mock_path.return_value.is_file.return_value = True
mock_sysinfo = pd.DataFrame({"other_column": ["value"]})
mock_read_csv.return_value = mock_sysinfo
connector = DatabaseConnector(mock_args_import)
with patch(
"db_connector.console_error", side_effect=SystemExit(1)
) as mock_console_error:
with pytest.raises(SystemExit):
connector.prep_import()
assert mock_console_error.called
error_call = mock_console_error.call_args[0][0]
assert "Outdated workload" in error_call
@patch("db_connector.tqdm")
@patch("db_connector.os.listdir")
@patch("db_connector.console_log")
@patch("db_connector.console_warning")
@patch("db_connector.kernel_name_shortener")
@patch("db_connector.MongoClient")
@patch("db_connector.pd.read_csv")
def test_db_import_success(
self,
mock_read_csv,
mock_mongo_client,
mock_kernel_shortener,
mock_console_warning,
mock_console_log,
mock_listdir,
mock_tqdm,
mock_args_import,
):
"""Test successful database import"""
mock_listdir.return_value = ["test_data.csv", "empty_file.csv", "non_csv.txt"]
mock_tqdm.return_value = mock_listdir.return_value
test_df = pd.DataFrame({"col1": [1, 2], "col2": [3, 4]})
mock_read_csv.side_effect = [test_df, pd.errors.EmptyDataError()]
mock_client_instance = MagicMock()
mock_db = MagicMock()
mock_collection = MagicMock()
mock_workload_db = MagicMock()
mock_workload_col = MagicMock()
mock_mongo_client.return_value = mock_client_instance
mock_client_instance.__getitem__.side_effect = lambda x: {
"rocprofiler-compute_test_team_test_workload_MI100": mock_db,
"workload_names": mock_workload_db,
}.get(x, mock_db)
mock_db.__getitem__.return_value = mock_collection
mock_workload_db.__getitem__.return_value = mock_workload_col
connector = DatabaseConnector(mock_args_import)
connector.connection_info["workload"] = "/fake/workload/path"
connector.client = mock_client_instance
with patch.object(connector, "prep_import") as mock_prep:
mock_prep.return_value = None
connector.connection_info[
"db"
] = "rocprofiler-compute_test_team_test_workload_MI100"
connector.db_import()
mock_collection.insert_many.assert_called_once()
mock_workload_col.replace_one.assert_called_once()
@patch("db_connector.console_log")
def test_db_remove_success(self, mock_console_log, mock_args_remove):
"""Test successful database removal"""
mock_client = MagicMock()
mock_db_to_remove = MagicMock()
mock_workload_names_db = MagicMock()
mock_names_col = MagicMock()
mock_client.__getitem__.side_effect = lambda x: {
"rocprofiler-compute_test_team_workload_mi100": mock_db_to_remove,
"workload_names": mock_workload_names_db,
}[x]
mock_workload_names_db.__getitem__.return_value = mock_names_col
mock_db_to_remove.list_collection_names.return_value = ["col1", "col2"]
connector = DatabaseConnector(mock_args_remove)
connector.client = mock_client
connector.db_remove()
mock_client.drop_database.assert_called_once_with(mock_db_to_remove)
mock_names_col.delete_many.assert_called_once_with(
{"name": "rocprofiler-compute_test_team_workload_mi100"}
)
def test_pre_processing_no_action_specified(self, mock_args_import):
"""Test pre_processing when neither upload nor remove is specified"""
mock_args_import.upload = False
mock_args_import.remove = False
connector = DatabaseConnector(mock_args_import)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
def test_pre_processing_remove_invalid_workload_name(self, mock_args_remove):
"""Test pre_processing remove with invalid workload name"""
mock_args_remove.workload = "invalid_name"
connector = DatabaseConnector(mock_args_remove)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
def test_pre_processing_remove_missing_host_username(self, mock_args_remove):
"""Test pre_processing remove with missing host/username"""
mock_args_remove.host = None
mock_args_remove.username = None
connector = DatabaseConnector(mock_args_remove)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
def test_pre_processing_remove_protected_database(self, mock_args_remove):
"""Test pre_processing remove with protected database names"""
mock_args_remove.workload = "admin"
connector = DatabaseConnector(mock_args_remove)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
@patch("db_connector.Path")
@patch("db_connector.is_workload_empty")
@patch("db_connector.getpass.getpass")
@patch("db_connector.console_log")
@patch("db_connector.MongoClient")
def test_pre_processing_import_password_prompt_success(
self,
mock_mongo_client,
mock_console_log,
mock_getpass,
mock_is_workload_empty,
mock_path,
mock_args_import,
):
"""Test pre_processing import with password prompt success"""
mock_args_import.password = ""
mock_getpass.return_value = "prompted_password"
mock_path.return_value.absolute.return_value.is_dir.return_value = True
mock_path.return_value.absolute.return_value.resolve.return_value = (
"/resolved/path"
)
mock_client_instance = MagicMock()
mock_mongo_client.return_value = mock_client_instance
mock_client_instance.server_info.return_value = {}
connector = DatabaseConnector(mock_args_import)
connector.pre_processing()
mock_getpass.assert_called_once()
mock_console_log.assert_called_with("database", "Password received")
@patch("db_connector.Path")
@patch("db_connector.is_workload_empty")
@patch("db_connector.MongoClient")
def test_pre_processing_import_connection_failure(
self, mock_mongo_client, mock_is_workload_empty, mock_path, mock_args_import
):
"""Test pre_processing import with MongoDB connection failure"""
mock_path.return_value.absolute.return_value.is_dir.return_value = True
mock_path.return_value.absolute.return_value.resolve.return_value = (
"/resolved/path"
)
mock_client_instance = MagicMock()
mock_mongo_client.return_value = mock_client_instance
mock_client_instance.server_info.side_effect = Exception("Connection failed")
connector = DatabaseConnector(mock_args_import)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
@patch("db_connector.Path")
@patch("db_connector.is_workload_empty")
def test_pre_processing_import_missing_required_fields(
self, mock_is_workload_empty, mock_path, mock_args_import
):
"""Test pre_processing import with missing required fields"""
mock_args_import.host = None
connector = DatabaseConnector(mock_args_import)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
@patch("db_connector.Path")
def test_pre_processing_import_invalid_workload_path(
self, mock_path, mock_args_import
):
"""Test pre_processing import with invalid workload path"""
mock_path.return_value.absolute.return_value.is_dir.return_value = False
connector = DatabaseConnector(mock_args_import)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
def test_pre_processing_import_team_name_too_long(self, mock_args_import):
"""Test pre_processing import with team name exceeding limit"""
mock_args_import.team = "this_team_name_is_way_too_long"
connector = DatabaseConnector(mock_args_import)
with patch("db_connector.console_error", side_effect=SystemExit(1)):
with pytest.raises(SystemExit):
connector.pre_processing()
class TestDatabaseConnectorIntegration:
"""Simple integration test"""
@patch("db_connector.Path")
@patch("db_connector.pd.read_csv")
def test_prep_import_with_real_workload_path(self, mock_read_csv, mock_path):
"""Test prep_import with actual workload path structure"""
args = Mock()
args.username = "test_user"
args.password = "test_pass"
args.host = "localhost"
args.port = 27017
args.team = "test_team"
args.workload = "/app/tests/workloads/device_filter/MI100"
args.upload = True
args.remove = False
args.kernel_verbose = False
mock_path.return_value.joinpath.return_value = (
"/app/tests/workloads/device_filter/MI100/sysinfo.csv"
)
mock_path.return_value.is_file.return_value = True
mock_sysinfo = pd.DataFrame(
{"gpu_model": ["MI100"], "workload_name": ["device_filter"]}
)
mock_read_csv.return_value = mock_sysinfo
connector = DatabaseConnector(args)
connector.prep_import()
expected_db = "rocprofiler-compute_test_team_device_filter_MI100"
assert connector.connection_info["db"] == expected_db
if __name__ == "__main__":
pytest.main([__file__, "-v"])