diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index f03b22b..ea94844 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -9,12 +9,12 @@ on: jobs: tests: - runs-on: ubuntu-24.04 - timeout-minutes: 30 + name: Run tests + runs-on: ${{ matrix.os }} strategy: matrix: + os: [ubuntu-24.04, windows-2025, macos-15] python-version: ["3.11", "3.12", "3.13"] - name: Run tests steps: - name: Checkout repo uses: actions/checkout@v5.0.0 @@ -36,7 +36,7 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} docs: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v5.0.0 diff --git a/.gitignore b/.gitignore index f64f3de..a477332 100644 --- a/.gitignore +++ b/.gitignore @@ -110,6 +110,7 @@ ENV/ *.csv Example_output/ Example/data/*.csv +*.lock # profraw files from LLVM? Unclear exactly what triggers this # There are reports this comes from LLVM profiling, but also Xcode 9. diff --git a/docs/developer_guide.rst b/docs/developer_guide.rst index 259d21b..7fe201a 100644 --- a/docs/developer_guide.rst +++ b/docs/developer_guide.rst @@ -19,7 +19,7 @@ Clone the repository:: Install development dependencies:: - pip install -e .[testing,docs,pre-commit] + pip install -e ".[testing,docs,pre-commit]" Running Tests ------------- diff --git a/docs/getting_started.rst b/docs/getting_started.rst index c677653..2a440d3 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -57,9 +57,6 @@ The program assumes the following default unit Quick start guide -------------------- -.. Warning:: - - CodeEntropy has not been tested on Windows A quick and easy way to get started is to use the command-line tool which you can run in bash by simply typing ``CodeEntropy`` diff --git a/tests/test_CodeEntropy/test_arg_config_manager.py b/tests/test_CodeEntropy/test_arg_config_manager.py index 6aa15a8..bf6d220 100644 --- a/tests/test_CodeEntropy/test_arg_config_manager.py +++ b/tests/test_CodeEntropy/test_arg_config_manager.py @@ -1,27 +1,24 @@ import argparse import logging import os -import shutil -import tempfile import unittest from unittest.mock import MagicMock, mock_open, patch import tests.data as data from CodeEntropy.config.arg_config_manager import ConfigManager from CodeEntropy.main import main +from tests.test_CodeEntropy.test_base import BaseTestCase -class test_arg_config_manager(unittest.TestCase): +class TestArgConfigManager(BaseTestCase): """ Unit tests for the ConfigManager. """ def setUp(self): - """ - Setup test data and output directories. - """ + super().setUp() + self.test_data_dir = os.path.dirname(data.__file__) - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") self.config_file = os.path.join(self.test_dir, "config.yaml") # Create a mock config file @@ -30,18 +27,6 @@ def setUp(self): with open(self.config_file, "w") as f: f.write(mock_file.return_value.read()) - # Change to test directory - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) - - def tearDown(self): - """ - Clean up after each test. - """ - os.chdir(self._orig_dir) - if os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir) - def list_data_files(self): """ List all files in the test data directory. diff --git a/tests/test_CodeEntropy/test_base.py b/tests/test_CodeEntropy/test_base.py new file mode 100644 index 0000000..2c4da9a --- /dev/null +++ b/tests/test_CodeEntropy/test_base.py @@ -0,0 +1,45 @@ +import os +import shutil +import tempfile +import unittest + + +class BaseTestCase(unittest.TestCase): + """ + Base test case class for cross-platform unit tests. + + Provides: + 1. A unique temporary directory for each test to avoid filesystem conflicts. + 2. Automatic restoration of the working directory after each test. + 3. Prepares a logs folder path for tests that need logging configuration. + """ + + def setUp(self): + """ + Prepare the test environment before each test method runs. + + Actions performed: + 1. Creates a unique temporary directory for the test. + 2. Creates a 'logs' subdirectory within the temp directory. + 3. Changes the current working directory to the temporary directory. + """ + # Create a unique temporary test directory + self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") + self.logs_path = os.path.join(self.test_dir, "logs") + os.makedirs(self.logs_path, exist_ok=True) + + self._orig_dir = os.getcwd() + os.chdir(self.test_dir) + + def tearDown(self): + """ + Clean up the test environment after each test method runs. + + Actions performed: + 1. Restores the original working directory. + 2. Deletes the temporary test directory along with all its contents. + """ + os.chdir(self._orig_dir) + + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir, ignore_errors=True) diff --git a/tests/test_CodeEntropy/test_data_logger.py b/tests/test_CodeEntropy/test_data_logger.py index db0db9d..9e679c8 100644 --- a/tests/test_CodeEntropy/test_data_logger.py +++ b/tests/test_CodeEntropy/test_data_logger.py @@ -1,7 +1,4 @@ import json -import os -import shutil -import tempfile import unittest import numpy as np @@ -10,38 +7,20 @@ from CodeEntropy.config.data_logger import DataLogger from CodeEntropy.config.logging_config import LoggingConfig from CodeEntropy.main import main +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestDataLogger(unittest.TestCase): +class TestDataLogger(BaseTestCase): """ - Unit tests for the DataLogger class. These tests verify the - correct behavior of data logging, JSON export, and table - logging functionalities. + Unit tests for the DataLogger class. """ def setUp(self): - """ - Set up a temporary test environment before each test. - Creates a temporary directory and initializes a DataLogger instance. - """ - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") + super().setUp() self.code_entropy = main - - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) - self.logger = DataLogger() self.output_file = "test_output.json" - def tearDown(self): - """ - Clean up the test environment after each test. - Removes the temporary directory and restores the original working directory. - """ - os.chdir(self._orig_dir) - if os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir) - def test_init(self): """ Test that the DataLogger initializes with empty molecule and residue data lists. diff --git a/tests/test_CodeEntropy/test_entropy.py b/tests/test_CodeEntropy/test_entropy.py index f7af3bb..60f74a2 100644 --- a/tests/test_CodeEntropy/test_entropy.py +++ b/tests/test_CodeEntropy/test_entropy.py @@ -1,3 +1,4 @@ +import logging import math import os import shutil @@ -20,32 +21,21 @@ from CodeEntropy.levels import LevelManager from CodeEntropy.main import main from CodeEntropy.run import ConfigManager, RunManager +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestEntropyManager(unittest.TestCase): +class TestEntropyManager(BaseTestCase): """ - Unit tests for the functionality of EntropyManager. + Unit tests for EntropyManager. """ def setUp(self): - """ - Set up test environment. - """ - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") + super().setUp() self.test_data_dir = os.path.dirname(data.__file__) - self.code_entropy = main - - # Change to test directory - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) - def tearDown(self): - """ - Clean up after each test. - """ - os.chdir(self._orig_dir) - if os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir) + # Disable MDAnalysis and commands file logging entirely + logging.getLogger("MDAnalysis").handlers = [logging.NullHandler()] + logging.getLogger("commands").handlers = [logging.NullHandler()] def test_execute_full_workflow(self): # Setup universe and args @@ -56,7 +46,7 @@ def test_execute_full_workflow(self): args = MagicMock( bin_width=0.1, temperature=300, selection_string="all", water_entropy=False ) - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -153,7 +143,7 @@ def test_execute_triggers_handle_water_entropy_minimal(self): args = MagicMock( bin_width=0.1, temperature=300, selection_string="all", water_entropy=True ) - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -279,7 +269,7 @@ def test_initialize_molecules(self): args = MagicMock( bin_width=0.1, temperature=300, selection_string="all", water_entropy=False ) - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -486,7 +476,7 @@ def test_get_reduced_universe_reduced(self, mock_args): u = mda.Universe(tprfile, trrfile) config_manager = ConfigManager() - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") parser = config_manager.setup_argparse() args = parser.parse_args() @@ -524,7 +514,7 @@ def test_get_molecule_container(self, mock_args): # Setup managers config_manager = ConfigManager() - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") parser = config_manager.setup_argparse() args = parser.parse_args() @@ -639,7 +629,7 @@ def test_process_vibrational_only_levels(self): # Setup managers and arguments args = MagicMock(bin_width=0.1, temperature=300, selection_string="all") - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -751,7 +741,7 @@ def test_process_conformational_residue_level(self): # Setup managers and arguments args = MagicMock(bin_width=0.1, temperature=300, selection_string="all") - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -1086,7 +1076,7 @@ def test_vibrational_entropy_init(self): args.temperature = 300 args.selection_string = "all" - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -1111,7 +1101,7 @@ def test_frequency_calculation_0(self): lambdas = [0] temp = 298 - run_manager = RunManager("mock_folder") + run_manager = RunManager("mock_folder/job001") ve = VibrationalEntropy( run_manager, MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock() @@ -1131,7 +1121,7 @@ def test_frequency_calculation_positive(self): temp = 298 # Create a mock RunManager and set return value for get_KT2J - run_manager = RunManager("mock_folder") + run_manager = RunManager("mock_folder/job001") # Instantiate VibrationalEntropy with mocks ve = VibrationalEntropy( @@ -1273,7 +1263,7 @@ def test_vibrational_entropy_polymer_force(self): temp = 298 highest_level = "yes" - run_manager = RunManager("mock_folder") + run_manager = RunManager("mock_folder/job001") ve = VibrationalEntropy( run_manager, MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock() ) @@ -1303,7 +1293,7 @@ def test_vibrational_entropy_polymer_torque(self): temp = 298 highest_level = "yes" - run_manager = RunManager("mock_folder") + run_manager = RunManager("mock_folder/job001") ve = VibrationalEntropy( run_manager, MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock() ) @@ -1561,7 +1551,7 @@ def test_confirmational_entropy_init(self): args.temperature = 300 args.selection_string = "all" - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -1603,7 +1593,7 @@ def test_assign_conformation(self): # Setup managers and arguments args = MagicMock(bin_width=0.1, temperature=300, selection_string="all") - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -1635,7 +1625,7 @@ def test_conformational_entropy_calculation(self): # Setup managers and arguments args = MagicMock(bin_width=0.1, temperature=300, selection_string="all") - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() @@ -1697,7 +1687,7 @@ def test_orientational_entropy_init(self): args.temperature = 300 args.selection_string = "all" - run_manager = RunManager("temp_folder") + run_manager = RunManager("mock_folder/job001") level_manager = LevelManager() data_logger = DataLogger() group_molecules = MagicMock() diff --git a/tests/test_CodeEntropy/test_group_molecules.py b/tests/test_CodeEntropy/test_group_molecules.py index 1914e5e..6f15805 100644 --- a/tests/test_CodeEntropy/test_group_molecules.py +++ b/tests/test_CodeEntropy/test_group_molecules.py @@ -1,37 +1,21 @@ -import os -import shutil -import tempfile import unittest from unittest.mock import MagicMock import numpy as np from CodeEntropy.group_molecules import GroupMolecules +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestMain(unittest.TestCase): +class TestGroupMolecules(BaseTestCase): """ - Unit tests for the functionality of GroupMolecules class. + Unit tests for GroupMolecules. """ def setUp(self): - """ - Set up a temporary directory as the working directory before each test. - Initialize GroupMolecules instance. - """ - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) + super().setUp() self.group_molecules = GroupMolecules() - def tearDown(self): - """ - Clean up by removing the temporary directory and restoring the original working - directory. - """ - os.chdir(self._orig_dir) - shutil.rmtree(self.test_dir) - def test_by_none_returns_individual_groups(self): """ Test _by_none returns each molecule in its own group when grouping is 'each'. diff --git a/tests/test_CodeEntropy/test_levels.py b/tests/test_CodeEntropy/test_levels.py index bf49ab2..4d0380f 100644 --- a/tests/test_CodeEntropy/test_levels.py +++ b/tests/test_CodeEntropy/test_levels.py @@ -1,38 +1,18 @@ -import os -import shutil -import tempfile -import unittest from unittest.mock import MagicMock, patch import numpy as np from CodeEntropy.levels import LevelManager -from CodeEntropy.main import main +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestLevels(unittest.TestCase): +class TestLevels(BaseTestCase): """ - Unit tests for the functionality of Levels. + Unit tests for Levels. """ def setUp(self): - """ - Set up test environment. - """ - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") - self.code_entropy = main - - # Change to test directory - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) - - def tearDown(self): - """ - Clean up after each test. - """ - os.chdir(self._orig_dir) - if os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir) + super().setUp() def test_select_levels(self): """ diff --git a/tests/test_CodeEntropy/test_logging_config.py b/tests/test_CodeEntropy/test_logging_config.py index 9640950..7a07b2a 100644 --- a/tests/test_CodeEntropy/test_logging_config.py +++ b/tests/test_CodeEntropy/test_logging_config.py @@ -1,26 +1,25 @@ import logging import os -import tempfile import unittest from unittest.mock import MagicMock from CodeEntropy.config.logging_config import LoggingConfig +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestLoggingConfig(unittest.TestCase): +class TestLoggingConfig(BaseTestCase): + """ + Unit tests for LoggingConfig. + """ def setUp(self): - # Use a temporary directory for logs - self.temp_dir = tempfile.TemporaryDirectory() - self.log_dir = os.path.join(self.temp_dir.name, "logs") - self.logging_config = LoggingConfig(folder=self.temp_dir.name) + super().setUp() + self.log_dir = self.logs_path + self.logging_config = LoggingConfig(folder=self.test_dir) self.mock_text = "Test console output" self.logging_config.console.export_text = MagicMock(return_value=self.mock_text) - def tearDown(self): - self.temp_dir.cleanup() - def test_log_directory_created(self): """Check if the log directory is created upon init""" self.assertTrue(os.path.exists(self.log_dir)) @@ -68,7 +67,7 @@ def test_update_logging_level(self): def test_mdanalysis_and_command_loggers_exist(self): """Ensure specialized loggers are set up with correct configuration""" log_level = logging.DEBUG - self.logging_config = LoggingConfig(folder=self.temp_dir.name, level=log_level) + self.logging_config = LoggingConfig(folder=self.test_dir, level=log_level) self.logging_config.setup_logging() mda_logger = logging.getLogger("MDAnalysis") @@ -87,7 +86,7 @@ def test_save_console_log_writes_file(self): filename = "test_log.txt" self.logging_config.save_console_log(filename) - output_path = os.path.join(self.temp_dir.name, "logs", filename) + output_path = os.path.join(self.test_dir, "logs", filename) # Check file exists self.assertTrue(os.path.exists(output_path)) diff --git a/tests/test_CodeEntropy/test_main.py b/tests/test_CodeEntropy/test_main.py index 1a60972..6db9dd8 100644 --- a/tests/test_CodeEntropy/test_main.py +++ b/tests/test_CodeEntropy/test_main.py @@ -2,33 +2,21 @@ import shutil import subprocess import sys -import tempfile import unittest from unittest.mock import MagicMock, patch from CodeEntropy.main import main +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestMain(unittest.TestCase): +class TestMain(BaseTestCase): """ Unit tests for the main functionality of CodeEntropy. """ def setUp(self): - """ - Set up a temporary directory as the working directory before each test. - """ - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) - - def tearDown(self): - """ - Clean up by removing the temporary directory and restoring the original working - directory. - """ - os.chdir(self._orig_dir) - shutil.rmtree(self.test_dir) + super().setUp() + self.code_entropy = main @patch("CodeEntropy.main.sys.exit") @patch("CodeEntropy.main.RunManager") @@ -41,7 +29,7 @@ def test_main_successful_run(self, mock_RunManager, mock_exit): mock_RunManager.return_value = mock_run_manager_instance # Simulate that RunManager.create_job_folder returns a folder - mock_RunManager.create_job_folder.return_value = "dummy_folder" + mock_RunManager.create_job_folder.return_value = "mock_folder/job001" # Simulate the successful completion of the run_entropy_workflow method mock_run_manager_instance.run_entropy_workflow.return_value = None @@ -71,7 +59,7 @@ def test_main_exception_triggers_exit( mock_RunManager.return_value = mock_run_manager_instance # Simulate that RunManager.create_job_folder returns a folder - mock_RunManager.create_job_folder.return_value = "dummy_folder" + mock_RunManager.create_job_folder.return_value = "mock_folder/job001" # Simulate an exception in the run_entropy_workflow method mock_run_manager_instance.run_entropy_workflow.side_effect = Exception( @@ -114,6 +102,8 @@ def test_main_entry_point_runs(self): result = subprocess.run( [ sys.executable, + "-X", + "utf8", "-m", "CodeEntropy.main", "--top_traj_file", @@ -122,7 +112,7 @@ def test_main_entry_point_runs(self): ], cwd=self.test_dir, capture_output=True, - text=True, + encoding="utf-8", ) self.assertEqual(result.returncode, 0) diff --git a/tests/test_CodeEntropy/test_run.py b/tests/test_CodeEntropy/test_run.py index 565e476..54def3e 100644 --- a/tests/test_CodeEntropy/test_run.py +++ b/tests/test_CodeEntropy/test_run.py @@ -1,6 +1,4 @@ import os -import shutil -import tempfile import unittest from io import StringIO from unittest.mock import MagicMock, mock_open, patch @@ -11,37 +9,24 @@ from rich.console import Console from CodeEntropy.run import RunManager +from tests.test_CodeEntropy.test_base import BaseTestCase -class TestRunManager(unittest.TestCase): +class TestRunManager(BaseTestCase): """ Unit tests for the RunManager class. These tests verify the correct behavior of run manager. """ def setUp(self): - """ - Set up a temporary directory as the working directory before each test. - """ - self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") + super().setUp() self.config_file = os.path.join(self.test_dir, "CITATION.cff") - - # Create a mock config file + # Create mock config with patch("builtins.open", new_callable=mock_open) as mock_file: self.setup_citation_file(mock_file) with open(self.config_file, "w") as f: f.write(mock_file.return_value.read()) - - self._orig_dir = os.getcwd() - os.chdir(self.test_dir) - - def tearDown(self): - """ - Clean up by removing the temporary directory and restoring the original working - directory. - """ - os.chdir(self._orig_dir) - shutil.rmtree(self.test_dir) + self.run_manager = RunManager(folder=self.test_dir) def setup_citation_file(self, mock_file): """ @@ -64,8 +49,9 @@ def test_create_job_folder_empty_directory(self, mock_listdir, mock_makedirs): mock_listdir.return_value = [] new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job001") - self.assertEqual(new_folder_path, expected_path) - mock_makedirs.assert_called_once_with(expected_path, exist_ok=True) + self.assertEqual( + os.path.realpath(new_folder_path), os.path.realpath(expected_path) + ) @patch("os.makedirs") @patch("os.listdir") @@ -77,8 +63,23 @@ def test_create_job_folder_with_existing_folders(self, mock_listdir, mock_makedi mock_listdir.return_value = ["job001", "job002", "job003"] new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job004") - self.assertEqual(new_folder_path, expected_path) - mock_makedirs.assert_called_once_with(expected_path, exist_ok=True) + + # Normalize paths cross-platform + normalized_new = os.path.normcase( + os.path.realpath(os.path.normpath(new_folder_path)) + ) + normalized_expected = os.path.normcase( + os.path.realpath(os.path.normpath(expected_path)) + ) + + self.assertEqual(normalized_new, normalized_expected) + + called_args, called_kwargs = mock_makedirs.call_args + normalized_called = os.path.normcase( + os.path.realpath(os.path.normpath(called_args[0])) + ) + self.assertEqual(normalized_called, normalized_expected) + self.assertTrue(called_kwargs.get("exist_ok", False)) @patch("os.makedirs") @patch("os.listdir") @@ -90,10 +91,24 @@ def test_create_job_folder_with_non_matching_folders( folders. """ mock_listdir.return_value = ["folderA", "another_one"] + new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job001") - self.assertEqual(new_folder_path, expected_path) - mock_makedirs.assert_called_once_with(expected_path, exist_ok=True) + + normalized_new = os.path.normcase( + os.path.realpath(os.path.normpath(new_folder_path)) + ) + normalized_expected = os.path.normcase( + os.path.realpath(os.path.normpath(expected_path)) + ) + self.assertEqual(normalized_new, normalized_expected) + + called_args, called_kwargs = mock_makedirs.call_args + normalized_called = os.path.normcase( + os.path.realpath(os.path.normpath(called_args[0])) + ) + self.assertEqual(normalized_called, normalized_expected) + self.assertTrue(called_kwargs.get("exist_ok", False)) @patch("os.makedirs") @patch("os.listdir") @@ -105,8 +120,21 @@ def test_create_job_folder_mixed_folder_names(self, mock_listdir, mock_makedirs) mock_listdir.return_value = ["job001", "abc", "job002", "random"] new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job003") - self.assertEqual(new_folder_path, expected_path) - mock_makedirs.assert_called_once_with(expected_path, exist_ok=True) + + normalized_new = os.path.normcase( + os.path.realpath(os.path.normpath(new_folder_path)) + ) + normalized_expected = os.path.normcase( + os.path.realpath(os.path.normpath(expected_path)) + ) + self.assertEqual(normalized_new, normalized_expected) + + called_args, called_kwargs = mock_makedirs.call_args + normalized_called = os.path.normcase( + os.path.realpath(os.path.normpath(called_args[0])) + ) + self.assertEqual(normalized_called, normalized_expected) + self.assertTrue(called_kwargs.get("exist_ok", False)) @patch("os.makedirs") @patch("os.listdir") @@ -123,8 +151,20 @@ def test_create_job_folder_with_invalid_job_suffix( new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job003") - self.assertEqual(new_folder_path, expected_path) - mock_makedirs.assert_called_once_with(expected_path, exist_ok=True) + normalized_new = os.path.normcase( + os.path.realpath(os.path.normpath(new_folder_path)) + ) + normalized_expected = os.path.normcase( + os.path.realpath(os.path.normpath(expected_path)) + ) + self.assertEqual(normalized_new, normalized_expected) + + called_args, called_kwargs = mock_makedirs.call_args + normalized_called = os.path.normcase( + os.path.realpath(os.path.normpath(called_args[0])) + ) + self.assertEqual(normalized_called, normalized_expected) + self.assertTrue(called_kwargs.get("exist_ok", False)) @patch("requests.get") def test_load_citation_data_success(self, mock_get): @@ -255,7 +295,7 @@ def test_run_entropy_workflow(self): Test the run_entropy_workflow method to ensure it initializes and executes correctly with mocked dependencies. """ - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") run_manager._logging_config = MagicMock() run_manager._config_manager = MagicMock() run_manager.load_citation_data = MagicMock() @@ -318,7 +358,7 @@ def test_run_configuration_warning(self): """ Test that a warning is logged when the config entry is not a dictionary. """ - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") run_manager._logging_config = MagicMock() run_manager._config_manager = MagicMock() run_manager.load_citation_data = MagicMock() @@ -367,7 +407,7 @@ def test_run_entropy_workflow_missing_traj_file(self): """ Test that a ValueError is raised when 'top_traj_file' is missing. """ - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") run_manager._logging_config = MagicMock() run_manager._config_manager = MagicMock() run_manager.load_citation_data = MagicMock() @@ -419,7 +459,7 @@ def test_run_entropy_workflow_missing_selection_string(self): """ Test that a ValueError is raised when 'selection_string' is missing. """ - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") run_manager._logging_config = MagicMock() run_manager._config_manager = MagicMock() run_manager.load_citation_data = MagicMock() @@ -504,7 +544,7 @@ def test_new_U_select_frame(self, MockMerge, MockAnalysisFromFunction): mock_merged_universe = MagicMock() MockMerge.return_value = mock_merged_universe - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") result = run_manager.new_U_select_frame(mock_universe) mock_universe.select_atoms.assert_called_once_with("all", updating=True) @@ -558,7 +598,7 @@ def test_new_U_select_atom(self, MockMerge, MockAnalysisFromFunction): mock_merged_universe = MagicMock() MockMerge.return_value = mock_merged_universe - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") result = run_manager.new_U_select_atom( mock_universe, select_string="resid 1-10" ) @@ -591,7 +631,7 @@ def test_write_universe(self, mock_open, mock_pickle_dump): mock_file = MagicMock() mock_open.return_value = mock_file - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") result = run_manager.write_universe(mock_universe, name="test_universe") mock_open.assert_called_once_with("test_universe.pkl", "wb") @@ -616,7 +656,7 @@ def test_read_universe(self, mock_open, mock_pickle_load): # Path to the mock file path = "test_universe.pkl" - run_manager = RunManager("folder") + run_manager = RunManager("mock_folder/job001") result = run_manager.read_universe(path) mock_open.assert_called_once_with(path, "rb")