Skip to content

Commit c5b4c32

Browse files
authored
Merge pull request #160 from CCPBioSim/159-expand-ci-testing
Expand CI pipeline to support `Windows` and `macOS`
2 parents f96e441 + 535ecbe commit c5b4c32

File tree

13 files changed

+188
-198
lines changed

13 files changed

+188
-198
lines changed

.github/workflows/project-ci.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ on:
99

1010
jobs:
1111
tests:
12-
runs-on: ubuntu-24.04
13-
timeout-minutes: 30
12+
name: Run tests
13+
runs-on: ${{ matrix.os }}
1414
strategy:
1515
matrix:
16+
os: [ubuntu-24.04, windows-2025, macos-15]
1617
python-version: ["3.11", "3.12", "3.13"]
17-
name: Run tests
1818
steps:
1919
- name: Checkout repo
2020
uses: actions/checkout@v5.0.0
@@ -36,7 +36,7 @@ jobs:
3636
github-token: ${{ secrets.GITHUB_TOKEN }}
3737

3838
docs:
39-
runs-on: ubuntu-24.04
39+
runs-on: ubuntu-latest
4040
timeout-minutes: 15
4141
steps:
4242
- uses: actions/checkout@v5.0.0

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ ENV/
110110
*.csv
111111
Example_output/
112112
Example/data/*.csv
113+
*.lock
113114

114115
# profraw files from LLVM? Unclear exactly what triggers this
115116
# There are reports this comes from LLVM profiling, but also Xcode 9.

docs/developer_guide.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Clone the repository::
1919

2020
Install development dependencies::
2121

22-
pip install -e .[testing,docs,pre-commit]
22+
pip install -e ".[testing,docs,pre-commit]"
2323

2424
Running Tests
2525
-------------

docs/getting_started.rst

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,6 @@ The program assumes the following default unit
5757

5858
Quick start guide
5959
--------------------
60-
.. Warning::
61-
62-
CodeEntropy has not been tested on Windows
6360

6461
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``
6562

tests/test_CodeEntropy/test_arg_config_manager.py

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,24 @@
11
import argparse
22
import logging
33
import os
4-
import shutil
5-
import tempfile
64
import unittest
75
from unittest.mock import MagicMock, mock_open, patch
86

97
import tests.data as data
108
from CodeEntropy.config.arg_config_manager import ConfigManager
119
from CodeEntropy.main import main
10+
from tests.test_CodeEntropy.test_base import BaseTestCase
1211

1312

14-
class test_arg_config_manager(unittest.TestCase):
13+
class TestArgConfigManager(BaseTestCase):
1514
"""
1615
Unit tests for the ConfigManager.
1716
"""
1817

1918
def setUp(self):
20-
"""
21-
Setup test data and output directories.
22-
"""
19+
super().setUp()
20+
2321
self.test_data_dir = os.path.dirname(data.__file__)
24-
self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_")
2522
self.config_file = os.path.join(self.test_dir, "config.yaml")
2623

2724
# Create a mock config file
@@ -30,18 +27,6 @@ def setUp(self):
3027
with open(self.config_file, "w") as f:
3128
f.write(mock_file.return_value.read())
3229

33-
# Change to test directory
34-
self._orig_dir = os.getcwd()
35-
os.chdir(self.test_dir)
36-
37-
def tearDown(self):
38-
"""
39-
Clean up after each test.
40-
"""
41-
os.chdir(self._orig_dir)
42-
if os.path.exists(self.test_dir):
43-
shutil.rmtree(self.test_dir)
44-
4530
def list_data_files(self):
4631
"""
4732
List all files in the test data directory.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import os
2+
import shutil
3+
import tempfile
4+
import unittest
5+
6+
7+
class BaseTestCase(unittest.TestCase):
8+
"""
9+
Base test case class for cross-platform unit tests.
10+
11+
Provides:
12+
1. A unique temporary directory for each test to avoid filesystem conflicts.
13+
2. Automatic restoration of the working directory after each test.
14+
3. Prepares a logs folder path for tests that need logging configuration.
15+
"""
16+
17+
def setUp(self):
18+
"""
19+
Prepare the test environment before each test method runs.
20+
21+
Actions performed:
22+
1. Creates a unique temporary directory for the test.
23+
2. Creates a 'logs' subdirectory within the temp directory.
24+
3. Changes the current working directory to the temporary directory.
25+
"""
26+
# Create a unique temporary test directory
27+
self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_")
28+
self.logs_path = os.path.join(self.test_dir, "logs")
29+
os.makedirs(self.logs_path, exist_ok=True)
30+
31+
self._orig_dir = os.getcwd()
32+
os.chdir(self.test_dir)
33+
34+
def tearDown(self):
35+
"""
36+
Clean up the test environment after each test method runs.
37+
38+
Actions performed:
39+
1. Restores the original working directory.
40+
2. Deletes the temporary test directory along with all its contents.
41+
"""
42+
os.chdir(self._orig_dir)
43+
44+
if os.path.exists(self.test_dir):
45+
shutil.rmtree(self.test_dir, ignore_errors=True)

tests/test_CodeEntropy/test_data_logger.py

Lines changed: 4 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import json
2-
import os
3-
import shutil
4-
import tempfile
52
import unittest
63

74
import numpy as np
@@ -10,38 +7,20 @@
107
from CodeEntropy.config.data_logger import DataLogger
118
from CodeEntropy.config.logging_config import LoggingConfig
129
from CodeEntropy.main import main
10+
from tests.test_CodeEntropy.test_base import BaseTestCase
1311

1412

15-
class TestDataLogger(unittest.TestCase):
13+
class TestDataLogger(BaseTestCase):
1614
"""
17-
Unit tests for the DataLogger class. These tests verify the
18-
correct behavior of data logging, JSON export, and table
19-
logging functionalities.
15+
Unit tests for the DataLogger class.
2016
"""
2117

2218
def setUp(self):
23-
"""
24-
Set up a temporary test environment before each test.
25-
Creates a temporary directory and initializes a DataLogger instance.
26-
"""
27-
self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_")
19+
super().setUp()
2820
self.code_entropy = main
29-
30-
self._orig_dir = os.getcwd()
31-
os.chdir(self.test_dir)
32-
3321
self.logger = DataLogger()
3422
self.output_file = "test_output.json"
3523

36-
def tearDown(self):
37-
"""
38-
Clean up the test environment after each test.
39-
Removes the temporary directory and restores the original working directory.
40-
"""
41-
os.chdir(self._orig_dir)
42-
if os.path.exists(self.test_dir):
43-
shutil.rmtree(self.test_dir)
44-
4524
def test_init(self):
4625
"""
4726
Test that the DataLogger initializes with empty molecule and residue data lists.

tests/test_CodeEntropy/test_entropy.py

Lines changed: 24 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import math
23
import os
34
import shutil
@@ -20,32 +21,21 @@
2021
from CodeEntropy.levels import LevelManager
2122
from CodeEntropy.main import main
2223
from CodeEntropy.run import ConfigManager, RunManager
24+
from tests.test_CodeEntropy.test_base import BaseTestCase
2325

2426

25-
class TestEntropyManager(unittest.TestCase):
27+
class TestEntropyManager(BaseTestCase):
2628
"""
27-
Unit tests for the functionality of EntropyManager.
29+
Unit tests for EntropyManager.
2830
"""
2931

3032
def setUp(self):
31-
"""
32-
Set up test environment.
33-
"""
34-
self.test_dir = tempfile.mkdtemp(prefix="CodeEntropy_")
33+
super().setUp()
3534
self.test_data_dir = os.path.dirname(data.__file__)
36-
self.code_entropy = main
37-
38-
# Change to test directory
39-
self._orig_dir = os.getcwd()
40-
os.chdir(self.test_dir)
4135

42-
def tearDown(self):
43-
"""
44-
Clean up after each test.
45-
"""
46-
os.chdir(self._orig_dir)
47-
if os.path.exists(self.test_dir):
48-
shutil.rmtree(self.test_dir)
36+
# Disable MDAnalysis and commands file logging entirely
37+
logging.getLogger("MDAnalysis").handlers = [logging.NullHandler()]
38+
logging.getLogger("commands").handlers = [logging.NullHandler()]
4939

5040
def test_execute_full_workflow(self):
5141
# Setup universe and args
@@ -56,7 +46,7 @@ def test_execute_full_workflow(self):
5646
args = MagicMock(
5747
bin_width=0.1, temperature=300, selection_string="all", water_entropy=False
5848
)
59-
run_manager = RunManager("temp_folder")
49+
run_manager = RunManager("mock_folder/job001")
6050
level_manager = LevelManager()
6151
data_logger = DataLogger()
6252
group_molecules = MagicMock()
@@ -153,7 +143,7 @@ def test_execute_triggers_handle_water_entropy_minimal(self):
153143
args = MagicMock(
154144
bin_width=0.1, temperature=300, selection_string="all", water_entropy=True
155145
)
156-
run_manager = RunManager("temp_folder")
146+
run_manager = RunManager("mock_folder/job001")
157147
level_manager = LevelManager()
158148
data_logger = DataLogger()
159149
group_molecules = MagicMock()
@@ -279,7 +269,7 @@ def test_initialize_molecules(self):
279269
args = MagicMock(
280270
bin_width=0.1, temperature=300, selection_string="all", water_entropy=False
281271
)
282-
run_manager = RunManager("temp_folder")
272+
run_manager = RunManager("mock_folder/job001")
283273
level_manager = LevelManager()
284274
data_logger = DataLogger()
285275
group_molecules = MagicMock()
@@ -486,7 +476,7 @@ def test_get_reduced_universe_reduced(self, mock_args):
486476
u = mda.Universe(tprfile, trrfile)
487477

488478
config_manager = ConfigManager()
489-
run_manager = RunManager("temp_folder")
479+
run_manager = RunManager("mock_folder/job001")
490480

491481
parser = config_manager.setup_argparse()
492482
args = parser.parse_args()
@@ -524,7 +514,7 @@ def test_get_molecule_container(self, mock_args):
524514

525515
# Setup managers
526516
config_manager = ConfigManager()
527-
run_manager = RunManager("temp_folder")
517+
run_manager = RunManager("mock_folder/job001")
528518

529519
parser = config_manager.setup_argparse()
530520
args = parser.parse_args()
@@ -639,7 +629,7 @@ def test_process_vibrational_only_levels(self):
639629

640630
# Setup managers and arguments
641631
args = MagicMock(bin_width=0.1, temperature=300, selection_string="all")
642-
run_manager = RunManager("temp_folder")
632+
run_manager = RunManager("mock_folder/job001")
643633
level_manager = LevelManager()
644634
data_logger = DataLogger()
645635
group_molecules = MagicMock()
@@ -751,7 +741,7 @@ def test_process_conformational_residue_level(self):
751741

752742
# Setup managers and arguments
753743
args = MagicMock(bin_width=0.1, temperature=300, selection_string="all")
754-
run_manager = RunManager("temp_folder")
744+
run_manager = RunManager("mock_folder/job001")
755745
level_manager = LevelManager()
756746
data_logger = DataLogger()
757747
group_molecules = MagicMock()
@@ -1086,7 +1076,7 @@ def test_vibrational_entropy_init(self):
10861076
args.temperature = 300
10871077
args.selection_string = "all"
10881078

1089-
run_manager = RunManager("temp_folder")
1079+
run_manager = RunManager("mock_folder/job001")
10901080
level_manager = LevelManager()
10911081
data_logger = DataLogger()
10921082
group_molecules = MagicMock()
@@ -1111,7 +1101,7 @@ def test_frequency_calculation_0(self):
11111101
lambdas = [0]
11121102
temp = 298
11131103

1114-
run_manager = RunManager("mock_folder")
1104+
run_manager = RunManager("mock_folder/job001")
11151105

11161106
ve = VibrationalEntropy(
11171107
run_manager, MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
@@ -1131,7 +1121,7 @@ def test_frequency_calculation_positive(self):
11311121
temp = 298
11321122

11331123
# Create a mock RunManager and set return value for get_KT2J
1134-
run_manager = RunManager("mock_folder")
1124+
run_manager = RunManager("mock_folder/job001")
11351125

11361126
# Instantiate VibrationalEntropy with mocks
11371127
ve = VibrationalEntropy(
@@ -1273,7 +1263,7 @@ def test_vibrational_entropy_polymer_force(self):
12731263
temp = 298
12741264
highest_level = "yes"
12751265

1276-
run_manager = RunManager("mock_folder")
1266+
run_manager = RunManager("mock_folder/job001")
12771267
ve = VibrationalEntropy(
12781268
run_manager, MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
12791269
)
@@ -1303,7 +1293,7 @@ def test_vibrational_entropy_polymer_torque(self):
13031293
temp = 298
13041294
highest_level = "yes"
13051295

1306-
run_manager = RunManager("mock_folder")
1296+
run_manager = RunManager("mock_folder/job001")
13071297
ve = VibrationalEntropy(
13081298
run_manager, MagicMock(), MagicMock(), MagicMock(), MagicMock(), MagicMock()
13091299
)
@@ -1561,7 +1551,7 @@ def test_confirmational_entropy_init(self):
15611551
args.temperature = 300
15621552
args.selection_string = "all"
15631553

1564-
run_manager = RunManager("temp_folder")
1554+
run_manager = RunManager("mock_folder/job001")
15651555
level_manager = LevelManager()
15661556
data_logger = DataLogger()
15671557
group_molecules = MagicMock()
@@ -1603,7 +1593,7 @@ def test_assign_conformation(self):
16031593

16041594
# Setup managers and arguments
16051595
args = MagicMock(bin_width=0.1, temperature=300, selection_string="all")
1606-
run_manager = RunManager("temp_folder")
1596+
run_manager = RunManager("mock_folder/job001")
16071597
level_manager = LevelManager()
16081598
data_logger = DataLogger()
16091599
group_molecules = MagicMock()
@@ -1635,7 +1625,7 @@ def test_conformational_entropy_calculation(self):
16351625

16361626
# Setup managers and arguments
16371627
args = MagicMock(bin_width=0.1, temperature=300, selection_string="all")
1638-
run_manager = RunManager("temp_folder")
1628+
run_manager = RunManager("mock_folder/job001")
16391629
level_manager = LevelManager()
16401630
data_logger = DataLogger()
16411631
group_molecules = MagicMock()
@@ -1697,7 +1687,7 @@ def test_orientational_entropy_init(self):
16971687
args.temperature = 300
16981688
args.selection_string = "all"
16991689

1700-
run_manager = RunManager("temp_folder")
1690+
run_manager = RunManager("mock_folder/job001")
17011691
level_manager = LevelManager()
17021692
data_logger = DataLogger()
17031693
group_molecules = MagicMock()

0 commit comments

Comments
 (0)