From 901f5116bb9cfa63486952c2301daa190d7790ea Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 12:58:26 +0100 Subject: [PATCH 01/16] ci: expand test matrix to include Windows and macOS runners --- .github/workflows/project-ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index f03b22b..dcd5331 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-latest, windows-latest, macos-latest] 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 From ed57f8f1212cd6e9789f6006267786800b0ef49f Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 12:59:47 +0100 Subject: [PATCH 02/16] include `.lock` files within `.gitignore` file --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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. From b1455b1fd10866c835bb8e450a219c33682f5798 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 13:19:08 +0100 Subject: [PATCH 03/16] remove `macos-latest` from test matrix build --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index dcd5331..c9c0db2 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest, windows-latest] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From ed8fd41915a8f3fd303eadb4adc393af3a1ae7d4 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 13:27:09 +0100 Subject: [PATCH 04/16] remove `windows-os-latest` from test matrix build --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index c9c0db2..ebb692d 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From a67fd9a0da9037b95fa01170fc09575807d8028e Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 14:10:47 +0100 Subject: [PATCH 05/16] change from `ubuntu-latest` os to `22.04` --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index ebb692d..360d32a 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest] + os: [ubuntu-24.04] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From 00e25d8c94698986e6700db9815724a80526f2a0 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 14:12:55 +0100 Subject: [PATCH 06/16] include `windows-2025` to the `project-ci.yaml` os matrix --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index 360d32a..a830df9 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04] + os: [ubuntu-24.04, windows-2025] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From d91d4e65b155e4f2ba3994cc5b97b73a8adfcf47 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 18:10:07 +0100 Subject: [PATCH 07/16] use `os.path.realpath()` to normalise paths within `test_run.py` --- tests/test_CodeEntropy/test_run.py | 37 ++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/tests/test_CodeEntropy/test_run.py b/tests/test_CodeEntropy/test_run.py index 565e476..cb42259 100644 --- a/tests/test_CodeEntropy/test_run.py +++ b/tests/test_CodeEntropy/test_run.py @@ -64,8 +64,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 +78,12 @@ 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) + self.assertEqual( + os.path.realpath(new_folder_path), os.path.realpath(expected_path) + ) + mock_makedirs.assert_called_once_with( + os.path.realpath(expected_path), exist_ok=True + ) @patch("os.makedirs") @patch("os.listdir") @@ -92,8 +97,12 @@ def test_create_job_folder_with_non_matching_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) + self.assertEqual( + os.path.realpath(new_folder_path), os.path.realpath(expected_path) + ) + mock_makedirs.assert_called_once_with( + os.path.realpath(expected_path), exist_ok=True + ) @patch("os.makedirs") @patch("os.listdir") @@ -105,8 +114,12 @@ 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) + self.assertEqual( + os.path.realpath(new_folder_path), os.path.realpath(expected_path) + ) + mock_makedirs.assert_called_once_with( + os.path.realpath(expected_path), exist_ok=True + ) @patch("os.makedirs") @patch("os.listdir") @@ -123,8 +136,12 @@ 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) + self.assertEqual( + os.path.realpath(new_folder_path), os.path.realpath(expected_path) + ) + mock_makedirs.assert_called_once_with( + os.path.realpath(expected_path), exist_ok=True + ) @patch("requests.get") def test_load_citation_data_success(self, mock_get): From 149dea30365b2e8cc2c57736abe26af6e7f77f90 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Wed, 17 Sep 2025 18:11:53 +0100 Subject: [PATCH 08/16] remove `windows-25` and replace it with `macos-15` within `project-ci.yaml` --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index a830df9..b66ee3d 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04, windows-2025] + os: [ubuntu-24.04, macos-15] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From 3d458a5368ea844dedd1abbc0d95bb251d229e6c Mon Sep 17 00:00:00 2001 From: Swift Date: Fri, 19 Sep 2025 16:12:22 +0100 Subject: [PATCH 09/16] Restructure `setup()` and `teardown()` to a unified format: - move the `setup()` and `teardown()` functions to a `test_base.py` file to centrally manage the setup and teardown of files - use `logging.shutdown()` to ensure logging handlers are correctly closed and shutdown after use to avoid file IO issues within tests - ensure the correct format of `jobxxx` folders are being used within tests ti ensure consistency --- .../test_arg_config_manager.py | 23 ++------ tests/test_CodeEntropy/test_base.py | 43 +++++++++++++++ tests/test_CodeEntropy/test_data_logger.py | 29 ++-------- tests/test_CodeEntropy/test_entropy.py | 55 +++++++------------ .../test_CodeEntropy/test_group_molecules.py | 24 ++------ tests/test_CodeEntropy/test_levels.py | 28 ++-------- tests/test_CodeEntropy/test_logging_config.py | 17 +++--- tests/test_CodeEntropy/test_main.py | 24 ++------ tests/test_CodeEntropy/test_run.py | 41 +++++--------- 9 files changed, 106 insertions(+), 178 deletions(-) create mode 100644 tests/test_CodeEntropy/test_base.py 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..468feb3 --- /dev/null +++ b/tests/test_CodeEntropy/test_base.py @@ -0,0 +1,43 @@ +import logging +import os +import shutil +import tempfile +import unittest +import uuid + + +class BaseTestCase(unittest.TestCase): + """ + Base class for tests with cross-platform setup and teardown. + Creates unique temporary directories and pre-creates expected log files. + """ + + def setUp(self): + # Unique temporary test directory + self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_") + self._orig_dir = os.getcwd() + os.chdir(self.test_dir) + + # Unique job folder + logs + self.job_id = f"job_{uuid.uuid4().hex[:6]}" + self.job_path = os.path.join(self.test_dir, self.job_id) + self.logs_path = os.path.join(self.job_path, "logs") + os.makedirs(self.logs_path, exist_ok=True) + + # Pre-create log files + for fname in ["mdanalysis.log", "program.log", "program.com"]: + with open(os.path.join(self.logs_path, fname), "w") as f: + f.write("") + + def tearDown(self): + # Shutdown logging and remove handlers (important for Windows) + logging.shutdown() + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + + # Restore working directory + os.chdir(self._orig_dir) + + # Remove temp directory (fail loudly if locked) + if os.path.exists(self.test_dir): + shutil.rmtree(self.test_dir, ignore_errors=False) 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..3a79f95 100644 --- a/tests/test_CodeEntropy/test_entropy.py +++ b/tests/test_CodeEntropy/test_entropy.py @@ -20,32 +20,17 @@ 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) def test_execute_full_workflow(self): # Setup universe and args @@ -56,7 +41,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 +138,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 +264,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 +471,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 +509,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 +624,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 +736,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 +1071,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 +1096,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 +1116,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 +1258,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 +1288,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 +1546,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 +1588,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 +1620,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 +1682,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..1120b27 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)) diff --git a/tests/test_CodeEntropy/test_main.py b/tests/test_CodeEntropy/test_main.py index 1a60972..f74fbbf 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( diff --git a/tests/test_CodeEntropy/test_run.py b/tests/test_CodeEntropy/test_run.py index cb42259..67e7289 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): """ @@ -272,7 +257,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() @@ -335,7 +320,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() @@ -384,7 +369,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() @@ -436,7 +421,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() @@ -521,7 +506,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) @@ -575,7 +560,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" ) @@ -608,7 +593,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") @@ -633,7 +618,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") From 297dd97dffa1d9d0815eef1b77da34528c7d8b43 Mon Sep 17 00:00:00 2001 From: Swift Date: Fri, 19 Sep 2025 16:18:14 +0100 Subject: [PATCH 10/16] remove `macos-15` from `project-ci.yam` to avoid GitHub Runner issue for `macos-15` --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index b66ee3d..360d32a 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04, macos-15] + os: [ubuntu-24.04] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From 113755cd0936873e6a111cde4b20b222b1cbff09 Mon Sep 17 00:00:00 2001 From: Swift Date: Mon, 22 Sep 2025 11:36:20 +0100 Subject: [PATCH 11/16] Refactor test setup and fix Unicode issues in CLI tests: - BaseTestCase: improved docstrings, simplified setup/teardown, pre-create logs folder, restore working directory after tests, ensure temp dir cleanup. - TestEntropyManager: disable MDAnalysis and commands loggers to prevent logging noise during tests. - TestLoggingConfig: use self.test_dir consistently instead of temp_dir.name for log file paths. - TestMain: force UTF-8 mode for subprocess (-X utf8) and capture output with UTF-8 encoding to avoid UnicodeEncodeErrors on Windows. - Inlcude `windows2025` within `project-ci.yaml` as the test cases now work on Windows --- .github/workflows/project-ci.yaml | 2 +- tests/test_CodeEntropy/test_base.py | 48 ++++++++++--------- tests/test_CodeEntropy/test_entropy.py | 5 ++ tests/test_CodeEntropy/test_logging_config.py | 4 +- tests/test_CodeEntropy/test_main.py | 4 +- 5 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index 360d32a..a830df9 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04] + os: [ubuntu-24.04, windows-2025] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo diff --git a/tests/test_CodeEntropy/test_base.py b/tests/test_CodeEntropy/test_base.py index 468feb3..2c4da9a 100644 --- a/tests/test_CodeEntropy/test_base.py +++ b/tests/test_CodeEntropy/test_base.py @@ -1,43 +1,45 @@ -import logging import os import shutil import tempfile import unittest -import uuid class BaseTestCase(unittest.TestCase): """ - Base class for tests with cross-platform setup and teardown. - Creates unique temporary directories and pre-creates expected log files. + 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): - # Unique temporary test directory + """ + 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._orig_dir = os.getcwd() - os.chdir(self.test_dir) - - # Unique job folder + logs - self.job_id = f"job_{uuid.uuid4().hex[:6]}" - self.job_path = os.path.join(self.test_dir, self.job_id) - self.logs_path = os.path.join(self.job_path, "logs") + self.logs_path = os.path.join(self.test_dir, "logs") os.makedirs(self.logs_path, exist_ok=True) - # Pre-create log files - for fname in ["mdanalysis.log", "program.log", "program.com"]: - with open(os.path.join(self.logs_path, fname), "w") as f: - f.write("") + self._orig_dir = os.getcwd() + os.chdir(self.test_dir) def tearDown(self): - # Shutdown logging and remove handlers (important for Windows) - logging.shutdown() - for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) + """ + Clean up the test environment after each test method runs. - # Restore working directory + Actions performed: + 1. Restores the original working directory. + 2. Deletes the temporary test directory along with all its contents. + """ os.chdir(self._orig_dir) - # Remove temp directory (fail loudly if locked) if os.path.exists(self.test_dir): - shutil.rmtree(self.test_dir, ignore_errors=False) + shutil.rmtree(self.test_dir, ignore_errors=True) diff --git a/tests/test_CodeEntropy/test_entropy.py b/tests/test_CodeEntropy/test_entropy.py index 3a79f95..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 @@ -32,6 +33,10 @@ def setUp(self): super().setUp() self.test_data_dir = os.path.dirname(data.__file__) + # 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 tprfile = os.path.join(self.test_data_dir, "md_A4_dna.tpr") diff --git a/tests/test_CodeEntropy/test_logging_config.py b/tests/test_CodeEntropy/test_logging_config.py index 1120b27..7a07b2a 100644 --- a/tests/test_CodeEntropy/test_logging_config.py +++ b/tests/test_CodeEntropy/test_logging_config.py @@ -67,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") @@ -86,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 f74fbbf..6db9dd8 100644 --- a/tests/test_CodeEntropy/test_main.py +++ b/tests/test_CodeEntropy/test_main.py @@ -102,6 +102,8 @@ def test_main_entry_point_runs(self): result = subprocess.run( [ sys.executable, + "-X", + "utf8", "-m", "CodeEntropy.main", "--top_traj_file", @@ -110,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) From 97b98db74ec8df523f89517c9d4fca7c2d4c30b1 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Mon, 22 Sep 2025 12:00:33 +0100 Subject: [PATCH 12/16] Fix `RunManager` job folder tests by using `os.path.realpath` for cross-platform path comparisons and proper handling of mixed, invalid, and non-job folder names --- tests/test_CodeEntropy/test_run.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/test_CodeEntropy/test_run.py b/tests/test_CodeEntropy/test_run.py index 67e7289..2c5fe54 100644 --- a/tests/test_CodeEntropy/test_run.py +++ b/tests/test_CodeEntropy/test_run.py @@ -63,11 +63,13 @@ 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( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) + os.path.normcase(os.path.realpath(new_folder_path)), + os.path.normcase(os.path.realpath(expected_path)), ) mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True + os.path.normcase(os.path.realpath(expected_path)), exist_ok=True ) @patch("os.makedirs") @@ -80,13 +82,16 @@ 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( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) + os.path.normcase(os.path.realpath(new_folder_path)), + os.path.normcase(os.path.realpath(expected_path)), ) mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True + os.path.normcase(os.path.realpath(expected_path)), exist_ok=True ) @patch("os.makedirs") @@ -100,12 +105,14 @@ def test_create_job_folder_mixed_folder_names(self, mock_listdir, mock_makedirs) new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job003") self.assertEqual( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) - ) - mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True + os.path.normcase(os.path.abspath(new_folder_path)), + os.path.normcase(os.path.abspath(expected_path)), ) + called_path = os.path.normcase(os.path.abspath(mock_makedirs.call_args[0][0])) + self.assertEqual(called_path, os.path.normcase(os.path.abspath(expected_path))) + self.assertTrue(mock_makedirs.call_args[1]["exist_ok"]) + @patch("os.makedirs") @patch("os.listdir") def test_create_job_folder_with_invalid_job_suffix( @@ -122,10 +129,11 @@ def test_create_job_folder_with_invalid_job_suffix( expected_path = os.path.join(self.test_dir, "job003") self.assertEqual( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) + os.path.normcase(os.path.realpath(new_folder_path)), + os.path.normcase(os.path.realpath(expected_path)), ) mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True + os.path.normcase(os.path.realpath(expected_path)), exist_ok=True ) @patch("requests.get") From 5fd793508ec75ba2f289724fd9a0fc83dbf86ba9 Mon Sep 17 00:00:00 2001 From: Swift Date: Mon, 22 Sep 2025 12:17:32 +0100 Subject: [PATCH 13/16] Normalize paths in `create_job_folder tests to fix` cross-platform Windows/Linux/macOS assertion failures --- tests/test_CodeEntropy/test_run.py | 44 ++++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/tests/test_CodeEntropy/test_run.py b/tests/test_CodeEntropy/test_run.py index 67e7289..63e59e7 100644 --- a/tests/test_CodeEntropy/test_run.py +++ b/tests/test_CodeEntropy/test_run.py @@ -63,12 +63,15 @@ 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( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) - ) - mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True - ) + + normalized_new = os.path.normcase(os.path.normpath(new_folder_path)) + normalized_expected = os.path.normcase(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.normpath(called_args[0])) + self.assertEqual(normalized_called, normalized_expected) + self.assertTrue(called_kwargs.get("exist_ok", False)) @patch("os.makedirs") @patch("os.listdir") @@ -82,12 +85,15 @@ def test_create_job_folder_with_non_matching_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( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) - ) - mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True - ) + + normalized_new = os.path.normcase(os.path.normpath(new_folder_path)) + normalized_expected = os.path.normcase(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.normpath(called_args[0])) + self.assertEqual(normalized_called, normalized_expected) + self.assertTrue(called_kwargs.get("exist_ok", False)) @patch("os.makedirs") @patch("os.listdir") @@ -121,12 +127,14 @@ 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( - os.path.realpath(new_folder_path), os.path.realpath(expected_path) - ) - mock_makedirs.assert_called_once_with( - os.path.realpath(expected_path), exist_ok=True - ) + normalized_new = os.path.normcase(os.path.normpath(new_folder_path)) + normalized_expected = os.path.normcase(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.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): From 67340c8e93aa8d2011281692ade31be86af3fbc3 Mon Sep 17 00:00:00 2001 From: Swift Date: Mon, 22 Sep 2025 12:50:32 +0100 Subject: [PATCH 14/16] include `macos-15` within the `project-ci.yaml` to extend the testing for macos --- .github/workflows/project-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/project-ci.yaml b/.github/workflows/project-ci.yaml index a830df9..ea94844 100644 --- a/.github/workflows/project-ci.yaml +++ b/.github/workflows/project-ci.yaml @@ -13,7 +13,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-24.04, windows-2025] + os: [ubuntu-24.04, windows-2025, macos-15] python-version: ["3.11", "3.12", "3.13"] steps: - name: Checkout repo From 761027d96c9232080e6096f0888ef156d8005731 Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Mon, 22 Sep 2025 16:31:04 +0100 Subject: [PATCH 15/16] use `os.path.normcase` and `os.path.realpath` to ensure compatability is MacOS --- tests/test_CodeEntropy/test_run.py | 57 ++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/tests/test_CodeEntropy/test_run.py b/tests/test_CodeEntropy/test_run.py index beed61b..54def3e 100644 --- a/tests/test_CodeEntropy/test_run.py +++ b/tests/test_CodeEntropy/test_run.py @@ -64,12 +64,20 @@ def test_create_job_folder_with_existing_folders(self, mock_listdir, mock_makedi new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job004") - normalized_new = os.path.normcase(os.path.normpath(new_folder_path)) - normalized_expected = os.path.normcase(os.path.normpath(expected_path)) + # 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.normpath(called_args[0])) + 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)) @@ -87,12 +95,18 @@ def test_create_job_folder_with_non_matching_folders( new_folder_path = RunManager.create_job_folder() expected_path = os.path.join(self.test_dir, "job001") - normalized_new = os.path.normcase(os.path.normpath(new_folder_path)) - normalized_expected = os.path.normcase(os.path.normpath(expected_path)) + 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.normpath(called_args[0])) + 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)) @@ -106,14 +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( - os.path.normcase(os.path.abspath(new_folder_path)), - os.path.normcase(os.path.abspath(expected_path)), + + 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_path = os.path.normcase(os.path.abspath(mock_makedirs.call_args[0][0])) - self.assertEqual(called_path, os.path.normcase(os.path.abspath(expected_path))) - self.assertTrue(mock_makedirs.call_args[1]["exist_ok"]) + 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") @@ -130,12 +151,18 @@ 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") - normalized_new = os.path.normcase(os.path.normpath(new_folder_path)) - normalized_expected = os.path.normcase(os.path.normpath(expected_path)) + 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.normpath(called_args[0])) + 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)) From 535ecbef0b2b92ef7fd1598bc9e5e5d7230f15bb Mon Sep 17 00:00:00 2001 From: harryswift01 Date: Mon, 22 Sep 2025 16:48:15 +0100 Subject: [PATCH 16/16] Updated documentation for multi-OS testing: - Removed the warning about Windows not being tested - Added quotation marks around the editable `pip install` command to ensure correct installation --- docs/developer_guide.rst | 2 +- docs/getting_started.rst | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) 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``